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 cf5f2b9  Support for visualizing test results in VSCode extension over 
LSP. (#2766)
cf5f2b9 is described below

commit cf5f2b93caad447a2a61927833cbf501bb5bba23
Author: Dusan Balek <dusan.ba...@oracle.com>
AuthorDate: Fri Feb 19 16:58:40 2021 +0100

    Support for visualizing test results in VSCode extension over LSP. (#2766)
---
 .../gradle/execute/GradleDaemonExecutor.java       |  27 +-
 ide/gsf.testrunner.ui/apichanges.xml               |  14 +
 ide/gsf.testrunner.ui/manifest.mf                  |   2 +-
 .../gsf/testrunner/ui/ResultDisplayHandler.java    |  22 +-
 .../modules/gsf/testrunner/ui/api/Manager.java     |  72 ++--
 .../testrunner/ui/api/TestMethodController.java    |  11 +-
 .../ui/api/TestResultDisplayHandler.java           | 220 ++++++++++
 ide/gsf.testrunner/apichanges.xml                  |  13 +
 ide/gsf.testrunner/manifest.mf                     |   2 +-
 ide/gsf.testrunner/nbproject/project.xml           |  45 +-
 .../nbcode/nbproject/platform.properties           |   3 -
 java/java.lsp.server/nbproject/project.xml         |  29 +-
 .../server/debugging/launch/NbLaunchDelegate.java  |  28 +-
 .../debugging/launch/NbLaunchRequestHandler.java   |   3 +-
 .../lsp/server/progress/TestProgressHandler.java   | 140 ++++++
 .../server/protocol/NbCodeClientCapabilities.java  |  24 +-
 .../lsp/server/protocol/NbCodeClientWrapper.java   |   5 +
 .../lsp/server/protocol/NbCodeLanguageClient.java  |   9 +
 .../java/lsp/server/protocol/QuickPickItem.java    |   2 +-
 .../modules/java/lsp/server/protocol/Server.java   |  12 +-
 .../lsp/server/protocol/ShowInputBoxParams.java    |   4 +-
 .../lsp/server/protocol/ShowQuickPickParams.java   |   4 +-
 ...InputBoxParams.java => TestProgressParams.java} |  63 +--
 .../java/lsp/server/protocol/TestSuiteInfo.java    | 477 +++++++++++++++++++++
 .../server/protocol/TextDocumentServiceImpl.java   |  80 ++--
 .../lsp/server/protocol/WorkspaceServiceImpl.java  | 129 +++++-
 .../server/progress/TestProgressHandlerTest.java   | 200 +++++++++
 .../java/lsp/server/protocol/ServerTest.java       |  41 +-
 java/java.lsp.server/vscode/package-lock.json      |  22 +-
 java/java.lsp.server/vscode/package.json           |  15 +-
 java/java.lsp.server/vscode/src/extension.ts       |  58 ++-
 java/java.lsp.server/vscode/src/protocol.ts        |  27 ++
 java/java.lsp.server/vscode/src/test/runTest.ts    |  14 +-
 java/java.lsp.server/vscode/src/testAdapter.ts     | 212 +++++++++
 java/junit.ui/manifest.mf                          |   2 +-
 java/junit.ui/nbproject/project.xml                |   2 +-
 .../junit/ui/actions/TestClassInfoTask.java        |  19 +-
 java/testng.ui/manifest.mf                         |   2 +-
 java/testng.ui/nbproject/project.xml               |   2 +-
 .../testng/ui/actions/TestClassInfoTask.java       |  30 +-
 .../bundles/vscode-test-adapter-util-0.7.1-license |  22 +
 41 files changed, 1888 insertions(+), 220 deletions(-)

diff --git 
a/extide/gradle/src/org/netbeans/modules/gradle/execute/GradleDaemonExecutor.java
 
b/extide/gradle/src/org/netbeans/modules/gradle/execute/GradleDaemonExecutor.java
index 219be06..58b7350 100644
--- 
a/extide/gradle/src/org/netbeans/modules/gradle/execute/GradleDaemonExecutor.java
+++ 
b/extide/gradle/src/org/netbeans/modules/gradle/execute/GradleDaemonExecutor.java
@@ -50,11 +50,10 @@ import org.gradle.tooling.GradleConnectionException;
 import org.gradle.tooling.GradleConnector;
 import org.gradle.tooling.ProgressEvent;
 import org.gradle.tooling.ProjectConnection;
+import org.gradle.tooling.events.ProgressListener;
 import org.netbeans.api.progress.ProgressHandle;
 import org.netbeans.api.project.ProjectInformation;
 import org.netbeans.api.project.ProjectUtils;
-import org.netbeans.modules.gradle.NbGradleProjectImpl;
-import org.netbeans.modules.gradle.api.NbGradleProject;
 import 
org.netbeans.modules.gradle.api.execute.GradleDistributionManager.GradleDistribution;
 import org.netbeans.modules.gradle.spi.GradleFiles;
 import org.netbeans.modules.gradle.spi.execute.GradleDistributionProvider;
@@ -63,10 +62,12 @@ import 
org.netbeans.spi.project.ui.support.BuildExecutionSupport;
 import org.openide.awt.StatusDisplayer;
 import org.openide.execution.ExecutorTask;
 import org.openide.filesystems.FileUtil;
+import org.openide.util.Lookup;
 import org.openide.util.NbBundle;
 import org.openide.util.NbBundle.Messages;
 import org.openide.util.Utilities;
 import org.openide.util.io.ReaderInputStream;
+import org.openide.util.lookup.Lookups;
 import org.openide.windows.IOColorPrint;
 import org.openide.windows.IOColors;
 import org.openide.windows.InputOutput;
@@ -128,6 +129,23 @@ public final class GradleDaemonExecutor extends 
AbstractGradleExecutor {
         final InputOutput ioput = getInputOutput();
         actionStatesAtStart();
         handle.start();
+
+        // BuildLauncher creates its own threads, need to note the effective 
Lookup and re-establish it in the listeners
+        final Lookup execLookup = Lookup.getDefault();
+
+        class ProgressLookupListener implements 
org.gradle.tooling.events.ProgressListener {
+            private final org.gradle.tooling.events.ProgressListener delegate;
+
+            public ProgressLookupListener(ProgressListener delegate) {
+                this.delegate = delegate;
+            }
+
+            @Override
+            public void statusChanged(org.gradle.tooling.events.ProgressEvent 
event) {
+                Lookups.executeWith(execLookup, () -> 
delegate.statusChanged(event));
+            }
+        }
+
         try {
 
             BuildExecutionSupport.registerRunningItem(item);
@@ -187,14 +205,14 @@ public final class GradleDaemonExecutor extends 
AbstractGradleExecutor {
             buildLauncher.setStandardOutput(outStream);
             buildLauncher.setStandardError(errStream);
             buildLauncher.addProgressListener((ProgressEvent pe) -> {
-                handle.progress(pe.getDescription());
+                Lookups.executeWith(execLookup, () -> 
handle.progress(pe.getDescription()));
             });
 
             buildLauncher.withCancellationToken(cancelTokenSource.token());
             if (config.getProject() != null) {
                 Collection<? extends GradleProgressListenerProvider> providers 
= 
config.getProject().getLookup().lookupAll(GradleProgressListenerProvider.class);
                 for (GradleProgressListenerProvider provider : providers) {
-                    
buildLauncher.addProgressListener(provider.getProgressListener(), 
provider.getSupportedOperationTypes());
+                    buildLauncher.addProgressListener(new 
ProgressLookupListener(provider.getProgressListener()), 
provider.getSupportedOperationTypes());
                 }
             }
             buildLauncher.run();
@@ -205,6 +223,7 @@ public final class GradleDaemonExecutor extends 
AbstractGradleExecutor {
         } catch (UncheckedException | BuildException ex) {
             if (!cancelling) {
                 
StatusDisplayer.getDefault().setStatusText(Bundle.BUILD_FAILED(getProjectName()));
+                gradleTask.finish(1);
             } else {
                 // This can happen if cancelling a Gradle build which is 
running
                 // an external aplication
diff --git a/ide/gsf.testrunner.ui/apichanges.xml 
b/ide/gsf.testrunner.ui/apichanges.xml
index 6e62dbe..60258a7 100644
--- a/ide/gsf.testrunner.ui/apichanges.xml
+++ b/ide/gsf.testrunner.ui/apichanges.xml
@@ -52,6 +52,20 @@
 <!-- ACTUAL CHANGES BEGIN HERE: -->
 
 <changes>
+    <change id="TestResultDisplayHandler">
+        <api name="CommonTestrunnerUIAPI"/>
+        <summary>Added API class and SPI interface for handlers displaying 
test results</summary>
+        <version major="1" minor="22"/>
+        <date day="19" month="2" year="2021"/>
+        <author login="dbalek"/>
+        <compatibility addition="yes"/>
+        <description>
+            Added API class and SPI interface for handlers displaying test 
results.
+            Allows for alternative handling of tests results (e.g. sending over
+            LSP to clients for display).
+        </description>
+        <class package="org.netbeans.modules.gsf.testrunner.ui.spi" 
name="TestResultDisplayHandler"/>
+    </change>
     <change id="TestCreatorConfiguration_validity">
         <api name="CommonTestrunnerUIAPI"/>
         <summary>Added methods to determine the validity of the configuration 
panel in the "Create Tests" dialog</summary>
diff --git a/ide/gsf.testrunner.ui/manifest.mf 
b/ide/gsf.testrunner.ui/manifest.mf
index 083f334..898943b 100644
--- a/ide/gsf.testrunner.ui/manifest.mf
+++ b/ide/gsf.testrunner.ui/manifest.mf
@@ -2,5 +2,5 @@ Manifest-Version: 1.0
 OpenIDE-Module: org.netbeans.modules.gsf.testrunner.ui
 OpenIDE-Module-Layer: org/netbeans/modules/gsf/testrunner/ui/layer.xml
 OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/gsf/testrunner/ui/Bundle.properties
-OpenIDE-Module-Specification-Version: 1.21
+OpenIDE-Module-Specification-Version: 1.22
 
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/ResultDisplayHandler.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/ResultDisplayHandler.java
index c4f8727..dd2a5f3 100644
--- 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/ResultDisplayHandler.java
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/ResultDisplayHandler.java
@@ -40,6 +40,7 @@ import org.netbeans.modules.gsf.testrunner.api.Report;
 import org.netbeans.modules.gsf.testrunner.api.TestSession;
 import org.netbeans.modules.gsf.testrunner.api.TestSuite;
 import org.netbeans.modules.gsf.testrunner.ui.api.Manager;
+import org.netbeans.modules.gsf.testrunner.ui.api.TestResultDisplayHandler;
 import org.openide.ErrorManager;
 import org.openide.util.Lookup;
 import org.openide.windows.IOContainer;
@@ -51,7 +52,7 @@ import org.openide.windows.OutputWriter;
  *
  * @author Marian Petras. Erno Mononen
  */
-public final class ResultDisplayHandler {
+public final class ResultDisplayHandler implements 
TestResultDisplayHandler.Spi<ResultDisplayHandler> {
 
     private static final Logger LOGGER = 
Logger.getLogger(ResultDisplayHandler.class.getName());
 
@@ -118,7 +119,7 @@ public final class ResultDisplayHandler {
                 dividerSettings.getLocation());
     }
 
-    public int getTotalTests() {
+    public int getTotalTests(final ResultDisplayHandler token) {
         return statisticsPanel.getTreePanel().getTotalTests();
     }
 
@@ -178,7 +179,7 @@ public final class ResultDisplayHandler {
 
     /**
      */
-    public void displayOutput(final String text, final boolean error) {
+    public void displayOutput(final ResultDisplayHandler token, final String 
text, final boolean error) {
 
         /* Called from the AntLogger's thread */
 
@@ -220,7 +221,7 @@ public final class ResultDisplayHandler {
      * @param  suiteName  name of the running suite; or {@code null} in the 
case
      *                    of anonymous suite
      */
-    public void displaySuiteRunning(String suiteName) {
+    public void displaySuiteRunning(final ResultDisplayHandler token, String 
suiteName) {
 
         synchronized (this) {
 
@@ -240,7 +241,7 @@ public final class ResultDisplayHandler {
      *
      * @param  suite  name of the running suite
      */
-    public void displaySuiteRunning(TestSuite suite) {
+    public void displaySuiteRunning(final ResultDisplayHandler token, 
TestSuite suite) {
         synchronized (this) {
             assert runningSuite == null;
             suite = (suite != null) ? suite : TestSuite.ANONYMOUS_TEST_SUITE;
@@ -254,7 +255,7 @@ public final class ResultDisplayHandler {
 
     /**
      */
-    public void displayReport(final Report report) {
+    public void displayReport(final ResultDisplayHandler token, final Report 
report) {
 
         synchronized (this) {
             if (treePanel == null) {
@@ -272,7 +273,7 @@ public final class ResultDisplayHandler {
 
     /**
      */
-    public void displayMessage(final String msg) {
+    public void displayMessage(final ResultDisplayHandler token, final String 
msg) {
 
         /* Called from the AntLogger's thread */
 
@@ -289,7 +290,7 @@ public final class ResultDisplayHandler {
 
     /**
      */
-    public void displayMessageSessionFinished(final String msg) {
+    public void displayMessageSessionFinished(final ResultDisplayHandler 
token, final String msg) {
 
         /* Called from the AntLogger's thread */
 
@@ -395,4 +396,9 @@ public final class ResultDisplayHandler {
     Lookup getLookup() {
         return l;
     }
+
+    @Override
+    public ResultDisplayHandler create(TestSession session) {
+        return null;
+    }
 }
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/Manager.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/Manager.java
index 94d37bc..2a8cca7 100644
--- 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/Manager.java
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/Manager.java
@@ -276,7 +276,7 @@ public final class Manager {
                        final String text,
                        final boolean error) {
 
-        final ResultDisplayHandler displayHandler = getDisplayHandler(session);
+        final TestResultDisplayHandler displayHandler = 
getDisplayHandler(session);
         displayHandler.displayOutput(text, error);
         displayInWindow(session, displayHandler);
     }
@@ -290,7 +290,7 @@ public final class Manager {
     public synchronized void displaySuiteRunning(final TestSession session,
                              final String suiteName) {
 
-        final ResultDisplayHandler displayHandler = getDisplayHandler(session);
+        final TestResultDisplayHandler displayHandler = 
getDisplayHandler(session);
         displayHandler.displaySuiteRunning(suiteName);
         displayInWindow(session, displayHandler);
     }
@@ -304,7 +304,7 @@ public final class Manager {
     public synchronized void displaySuiteRunning(final TestSession session,
                              final TestSuite suite) {
 
-        final ResultDisplayHandler displayHandler = getDisplayHandler(session);
+        final TestResultDisplayHandler displayHandler = 
getDisplayHandler(session);
         displayHandler.displaySuiteRunning(suite);
         displayInWindow(session, displayHandler);
     }
@@ -334,7 +334,7 @@ public final class Manager {
 
         /* Called from the AntLogger's thread */
         report.setCompleted(completed);
-        final ResultDisplayHandler displayHandler = getDisplayHandler(session);
+        final TestResultDisplayHandler displayHandler = 
getDisplayHandler(session);
         displayHandler.displayReport(report);
         displayInWindow(session, displayHandler);
     }
@@ -364,7 +364,7 @@ public final class Manager {
         
         /* Called from the AntLogger's thread */
 
-        final ResultDisplayHandler displayHandler = getDisplayHandler(session);
+        final TestResultDisplayHandler displayHandler = 
getDisplayHandler(session);
         displayInWindow(session, displayHandler, sessionEnd);
         if (!sessionEnd) {
             displayHandler.displayMessage(message);
@@ -376,7 +376,7 @@ public final class Manager {
     /**
      */
     private void displayInWindow(final TestSession session,
-                                 final ResultDisplayHandler displayHandler) {
+                                 final TestResultDisplayHandler 
displayHandler) {
          displayInWindow(session, displayHandler, false);
     }
 
@@ -388,7 +388,7 @@ public final class Manager {
         "LBL_NotificationDisplayer_NoTestsExecuted_title=No tests executed for 
project: {0}",
         "LBL_NotificationDisplayer_detailsText=Open Test Results Window"})
     private void displayInWindow(final TestSession session,
-                                 final ResultDisplayHandler displayHandler,
+                                 final TestResultDisplayHandler displayHandler,
                                  final boolean sessionEnd) {
         final boolean firstDisplay = (testSessions.add(session) == true);
 
@@ -447,9 +447,9 @@ public final class Manager {
      *
      */
     private class Displayer implements Runnable {
-        private final ResultDisplayHandler displayHandler;
+        private final TestResultDisplayHandler displayHandler;
         private final boolean promote;
-        Displayer(final ResultDisplayHandler displayHandler,
+        Displayer(final TestResultDisplayHandler displayHandler,
                   final boolean promote) {
             this.displayHandler = displayHandler;
             this.promote = promote;
@@ -465,48 +465,50 @@ public final class Manager {
     /** singleton of the <code>ResultDisplayHandler</code> */
     // the ResultDisplayHandler holds TestSession and is referenced from other
     // places so we use WeakReference, otherwise there would be memory leak
-    private Map<TestSession,WeakReference<ResultDisplayHandler>> 
displayHandlers;
+    private Map<TestSession,WeakReference<TestResultDisplayHandler>> 
displayHandlers;
     private Semaphore lock;
     /**
      */
     @NbBundle.Messages({"Null_Session_Error=Test session passed was null"})
-    private synchronized ResultDisplayHandler getDisplayHandler(final 
TestSession session) {
+    private synchronized TestResultDisplayHandler getDisplayHandler(final 
TestSession session) {
         // just in case a client passes null as a test session catch it early 
here
         assert session != null : Bundle.Null_Session_Error();
-        ResultDisplayHandler displayHandler = null;
+        TestResultDisplayHandler displayHandler = null;
         if (displayHandlers != null) {
-            WeakReference<ResultDisplayHandler> reference = 
displayHandlers.get(session);
+            WeakReference<TestResultDisplayHandler> reference = 
displayHandlers.get(session);
             if (reference != null) {
                 displayHandler = reference.get();
             }
         } else {
-            displayHandlers = new 
WeakHashMap<TestSession,WeakReference<ResultDisplayHandler>>(7);
+            displayHandlers = new 
WeakHashMap<TestSession,WeakReference<TestResultDisplayHandler>>(7);
         }
 
         if (displayHandler == null) {
-            displayHandler = new ResultDisplayHandler(session);
-            createIO(displayHandler);
-            displayHandlers.put(session, new 
WeakReference<ResultDisplayHandler>(displayHandler));
-            final ResultDisplayHandler dispHandler = displayHandler;
-            lock = new Semaphore(1);
-            try {
-                lock.acquire(1);
-            } catch (InterruptedException e) {
-                LOGGER.log(Level.FINE, "Current thread was interrupted while 
acquiring a permit: {0}", e);
-            }
-            Mutex.EVENT.writeAccess(new Runnable() {
+            displayHandler = TestResultDisplayHandler.create(session);
+            displayHandlers.put(session, new 
WeakReference<TestResultDisplayHandler>(displayHandler));
+            TestResultDisplayHandler.Spi handlerSpi = displayHandler.getSpi();
+            if (handlerSpi instanceof ResultDisplayHandler) {
+                createIO((ResultDisplayHandler)handlerSpi);
+                lock = new Semaphore(1);
+                try {
+                    lock.acquire(1);
+                } catch (InterruptedException e) {
+                    LOGGER.log(Level.FINE, "Current thread was interrupted 
while acquiring a permit: {0}", e);
+                }
+                Mutex.EVENT.writeAccess(new Runnable() {
 
-                @Override
-                public void run() {
-                    StatisticsPanel comp = (StatisticsPanel) 
dispHandler.getDisplayComponent().getLeftComponent();
-                    dispHandler.setTreePanel(comp.getTreePanel());
-                    lock.release();
+                    @Override
+                    public void run() {
+                        StatisticsPanel comp = (StatisticsPanel) 
((ResultDisplayHandler)handlerSpi).getDisplayComponent().getLeftComponent();
+                        
((ResultDisplayHandler)handlerSpi).setTreePanel(comp.getTreePanel());
+                        lock.release();
+                    }
+                });
+                try {
+                    lock.acquire(1);
+                } catch (InterruptedException e) {
+                    LOGGER.log(Level.FINE, "Current thread was interrupted 
while acquiring a permit: {0}", e);
                 }
-            });
-            try {
-                lock.acquire(1);
-            } catch (InterruptedException e) {
-                LOGGER.log(Level.FINE, "Current thread was interrupted while 
acquiring a permit: {0}", e);
             }
         }
         return displayHandler;
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodController.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodController.java
index 2107a4d..8ec2330 100644
--- 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodController.java
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodController.java
@@ -36,16 +36,22 @@ public class TestMethodController {
     }
 
     public static final class TestMethod {
+        private final String testClassName;
         private final SingleMethod method;
         private final Position start;
         private final Position end;
 
-        public TestMethod(SingleMethod method, Position start, Position end) {
+        public TestMethod(String testClassName, SingleMethod method, Position 
start, Position end) {
+            this.testClassName = testClassName;
             this.method = method;
             this.start = start;
             this.end = end;
         }
 
+        public String getTestClassName() {
+            return testClassName;
+        }
+
         public SingleMethod method() {
             return method;
         }
@@ -57,8 +63,5 @@ public class TestMethodController {
         public Position end() {
             return end;
         }
-
-        
     }
-
 }
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestResultDisplayHandler.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestResultDisplayHandler.java
new file mode 100644
index 0000000..e500e9f
--- /dev/null
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestResultDisplayHandler.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.gsf.testrunner.ui.api;
+
+import org.netbeans.modules.gsf.testrunner.api.Report;
+import org.netbeans.modules.gsf.testrunner.api.TestSession;
+import org.netbeans.modules.gsf.testrunner.api.TestSuite;
+import org.netbeans.modules.gsf.testrunner.ui.ResultDisplayHandler;
+import org.openide.util.Lookup;
+
+/**
+ * API class for handlers displaying test results.
+ *
+ * @since 1.22
+ * @author Dusan Balek
+ */
+public abstract class TestResultDisplayHandler {
+
+    /**
+     * Creates the {@link TestResultDisplayHandler} instance for the test 
session.
+     *
+     * @param session test session
+     * @return {@link TestResultDisplayHandler} instance
+     */
+    public static final TestResultDisplayHandler create(TestSession session) {
+        Spi provider = Lookup.getDefault().lookup(Spi.class);
+        if (provider != null) {
+            return new Impl<>(provider.create(session), provider);
+        } else {
+            return new Impl<>(null, new ResultDisplayHandler(session));
+        }
+    }
+
+    private TestResultDisplayHandler() {
+    }
+
+    /**
+     * Display output produced by running test.
+     *
+     * @param text output text
+     * @param error mark the output text as error
+     */
+    public abstract void displayOutput(String text, boolean error);
+
+    /**
+     * Display information that a test suite is running.
+     *
+     * @param suiteName name of the running suite; or {@code null} in the case
+     *                  of anonymous suite
+     */
+    public abstract void displaySuiteRunning(String suiteName);
+
+    /**
+     * Display information that a test suite is running.
+     *
+     * @param suite the running suite
+     */
+    public abstract void displaySuiteRunning(TestSuite suite);
+
+    /**
+     * Display test results.
+     *
+     * @param report summary report to display
+     */
+    public abstract void displayReport(Report report);
+
+    /**
+     * Display message produced by running test.
+     *
+     * @param message message to display
+     */
+    public abstract void displayMessage(String message);
+
+    /**
+     * Display information that a test session has finished.
+     *
+     * @param message message to display
+     */
+    public abstract void displayMessageSessionFinished(String message);
+
+    /**
+     * Return total number of tests in session if known.
+     *
+     * @return number of tests
+     */
+    public abstract int getTotalTests();
+
+    /**
+     * Interface providing SPI for {@link TestResultDisplayHandler}s.
+     * Instances should be registered in the default lookup.
+     */
+    public static interface Spi<T> {
+
+        /**
+         * Creates {@link Spi} instance for the test session.
+         * @param session test session
+         * @return {@link Spi} instance
+         */
+        public T create(TestSession session);
+
+        /**
+         * Display output produced by running test.
+         *
+         * @param text output text
+         * @param error mark the output text as error
+         */
+        public void displayOutput(T token, String text, boolean error);
+
+            /**
+         * Display information that a test suite is running.
+         *
+         * @param suiteName name of the running suite; or {@code null} in the 
case
+         *                  of anonymous suite
+         */
+        public void displaySuiteRunning(T token, String suiteName);
+
+        /**
+         * Display information that a test suite is running.
+         *
+         * @param suite the running suite
+         */
+        public void displaySuiteRunning(T token, TestSuite suite);
+
+        /**
+         * Display test results.
+         *
+         * @param report summary report to display
+         */
+        public void displayReport(T token, Report report);
+
+        /**
+         * Display message produced by running test.
+         *
+         * @param message message to display
+         */
+        public void displayMessage(T token, String message);
+
+        /**
+         * Display information that a test session has finished.
+         *
+         * @param message message to display
+         */
+        public void displayMessageSessionFinished(T token, String message);
+
+        /**
+         * Return total number of tests in session if known.
+         *
+         * @return number of tests
+         */
+        public int getTotalTests(T token);
+    }
+
+    private static final class Impl<T> extends TestResultDisplayHandler {
+
+        private final T token;
+        private final Spi<T> spi;
+
+        private Impl(T token, Spi<T> spi) {
+            this.token = token;
+            this.spi = spi;
+        }
+
+        public void displayOutput(String text, boolean error) {
+            spi.displayOutput(token, text, error);
+        }
+
+        @Override
+        public void displaySuiteRunning(String suiteName) {
+            spi.displaySuiteRunning(token, suiteName);
+        }
+
+        @Override
+        public void displaySuiteRunning(TestSuite suite) {
+            spi.displaySuiteRunning(token, suite);
+        }
+
+        @Override
+        public void displayReport(Report report) {
+            spi.displayReport(token, report);
+        }
+
+        @Override
+        public void displayMessage(String message) {
+            spi.displayMessage(token, message);
+        }
+
+        @Override
+        public void displayMessageSessionFinished(String message) {
+            spi.displayMessageSessionFinished(token, message);
+        }
+
+        @Override
+        public int getTotalTests() {
+            return spi.getTotalTests(token);
+        }
+
+        @Override
+        Spi<T> getSpi() {
+            return spi;
+        }
+    }
+
+    abstract Spi getSpi();
+}
diff --git a/ide/gsf.testrunner/apichanges.xml 
b/ide/gsf.testrunner/apichanges.xml
index f0dbc99..1a49bed 100644
--- a/ide/gsf.testrunner/apichanges.xml
+++ b/ide/gsf.testrunner/apichanges.xml
@@ -52,6 +52,19 @@
 <!-- ACTUAL CHANGES BEGIN HERE: -->
 
 <changes>
+    <change id="CommonTestrunnerAPI_Public">
+        <api name="CommonTestrunnerAPI"/>
+        <summary>Common Test Runner API made public</summary>
+        <version major="2" minor="22"/>
+        <date day="19" month="2" year="2021"/>
+        <author login="dbalek"/>
+        <compatibility addition="yes"/>
+        <description>
+            Common Test Runner API made public instead of long list of friend 
modules.
+        </description>
+        <package name="org.netbeans.modules.gsf.testrunner.api"/>
+        <package name="org.netbeans.modules.gsf.testrunner.plugin"/>
+    </change>
     <change id="TestCreatorProvider_Registration_Identifier">
         <api name="CommonTestrunnerAPI"/>
         <summary>Added identifier attribute in TestCreatorProvider 
@Registration</summary>
diff --git a/ide/gsf.testrunner/manifest.mf b/ide/gsf.testrunner/manifest.mf
index d684753..d4c8e00 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.21
+OpenIDE-Module-Specification-Version: 2.22
 
diff --git a/ide/gsf.testrunner/nbproject/project.xml 
b/ide/gsf.testrunner/nbproject/project.xml
index 54acb65..836606c 100644
--- a/ide/gsf.testrunner/nbproject/project.xml
+++ b/ide/gsf.testrunner/nbproject/project.xml
@@ -68,51 +68,10 @@
                     </run-dependency>
                 </dependency>
             </module-dependencies>
-            <friend-packages>
-                <friend>com.sun.tools.tuxedo.testrunner</friend>
-                <friend>org.netbeans.gradle.project</friend>
-                <friend>org.netbeans.modules.gradle.test</friend>
-                <friend>org.netbeans.modules.android.testrunner</friend>
-                <friend>org.netbeans.modules.cnd.cncppunit</friend>
-                <friend>org.netbeans.modules.cnd.testrunner</friend>
-                <friend>org.netbeans.modules.gototest</friend>
-                <friend>org.netbeans.modules.groovy.support</friend>
-                <friend>org.netbeans.modules.gsf.testrunner.ui</friend>
-                <friend>org.netbeans.modules.hudson.ui</friend>
-                <friend>org.netbeans.modules.java.testrunner</friend>
-                <friend>org.netbeans.modules.java.testrunner.ant</friend>
-                <friend>org.netbeans.modules.java.testrunner.ui</friend>
-                <friend>org.netbeans.modules.javascript.jstestdriver</friend>
-                <friend>org.netbeans.modules.javascript.karma</friend>
-                <friend>org.netbeans.modules.junit</friend>
-                <friend>org.netbeans.modules.junit.ant</friend>
-                <friend>org.netbeans.modules.junit.ant.ui</friend>
-                <friend>org.netbeans.modules.junit.ui</friend>
-                <friend>org.netbeans.modules.maven.junit</friend>
-                <friend>org.netbeans.modules.maven.junit.ui</friend>
-                <friend>org.netbeans.modules.mobility.j2meunit</friend>
-                <friend>org.netbeans.modules.mobility.project</friend>
-                <friend>org.netbeans.modules.php.atoum</friend>
-                <friend>org.netbeans.modules.php.codeception</friend>
-                <friend>org.netbeans.modules.php.nette.tester</friend>
-                <friend>org.netbeans.modules.php.phpunit</friend>
-                <friend>org.netbeans.modules.php.project</friend>
-                <friend>org.netbeans.modules.python.testrunner</friend>
-                <friend>org.netbeans.modules.ruby.testrunner</friend>
-                <friend>org.netbeans.modules.selenium2</friend>
-                <friend>org.netbeans.modules.selenium2.java</friend>
-                <friend>org.netbeans.modules.selenium2.maven</friend>
-                <friend>org.netbeans.modules.selenium2.php</friend>
-                <friend>org.netbeans.modules.selenium2.webclient</friend>
-                <friend>org.netbeans.modules.selenium2.webclient.mocha</friend>
-                
<friend>org.netbeans.modules.selenium2.webclient.protractor</friend>
-                <friend>org.netbeans.modules.testng</friend>
-                <friend>org.netbeans.modules.testng.ant</friend>
-                <friend>org.netbeans.modules.testng.ui</friend>
-                <friend>org.netbeans.modules.web.clientproject.api</friend>
+            <public-packages>
                 <package>org.netbeans.modules.gsf.testrunner.api</package>
                 <package>org.netbeans.modules.gsf.testrunner.plugin</package>
-            </friend-packages>
+            </public-packages>
         </data>
     </configuration>
 </project>
diff --git a/java/java.lsp.server/nbcode/nbproject/platform.properties 
b/java/java.lsp.server/nbcode/nbproject/platform.properties
index 3b34848..2723dce 100644
--- a/java/java.lsp.server/nbcode/nbproject/platform.properties
+++ b/java/java.lsp.server/nbcode/nbproject/platform.properties
@@ -162,7 +162,6 @@ disabled.modules=\
     org.netbeans.modules.gradle.kit,\
     org.netbeans.modules.gradle.persistence,\
     org.netbeans.modules.gradle.spring,\
-    org.netbeans.modules.gradle.test,\
     org.netbeans.modules.html.angular,\
     org.netbeans.modules.html.custom,\
     org.netbeans.modules.html.editor,\
@@ -258,8 +257,6 @@ disabled.modules=\
     org.netbeans.modules.maven.grammar,\
     org.netbeans.modules.maven.graph,\
     org.netbeans.modules.maven.hints,\
-    org.netbeans.modules.maven.junit,\
-    org.netbeans.modules.maven.junit.ui,\
     org.netbeans.modules.maven.kit,\
     org.netbeans.modules.maven.osgi,\
     org.netbeans.modules.maven.persistence,\
diff --git a/java/java.lsp.server/nbproject/project.xml 
b/java/java.lsp.server/nbproject/project.xml
index e9a1f99..1403771 100644
--- a/java/java.lsp.server/nbproject/project.xml
+++ b/java/java.lsp.server/nbproject/project.xml
@@ -165,11 +165,29 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    
<code-name-base>org.netbeans.modules.extexecution</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>2</release-version>
+                        <specification-version>1.59</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.gsf.testrunner</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>2</release-version>
+                        <specification-version>2.22</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     
<code-name-base>org.netbeans.modules.gsf.testrunner.ui</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>1.21</specification-version>
+                        <specification-version>1.22</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
@@ -216,6 +234,15 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    
<code-name-base>org.netbeans.modules.java.project</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.83</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     
<code-name-base>org.netbeans.modules.java.source</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
index cb1c2b9..fef4535 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java
@@ -22,7 +22,6 @@ import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
@@ -40,12 +39,13 @@ import org.netbeans.api.java.classpath.ClassPath;
 import org.netbeans.api.java.queries.UnitTestForSourceQuery;
 import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
+import org.netbeans.modules.java.lsp.server.Utils;
 import org.netbeans.modules.java.lsp.server.debugging.DebugAdapterContext;
 import org.netbeans.modules.java.lsp.server.debugging.NbSourceProvider;
 import org.netbeans.modules.java.lsp.server.progress.OperationContext;
 import org.netbeans.modules.java.lsp.server.progress.ProgressOperationEvent;
 import org.netbeans.modules.java.lsp.server.progress.ProgressOperationListener;
-import org.netbeans.modules.progress.spi.InternalHandle;
+import org.netbeans.modules.java.lsp.server.progress.TestProgressHandler;
 import org.netbeans.spi.project.ActionProgress;
 import org.netbeans.spi.project.ActionProvider;
 import org.netbeans.spi.project.SingleMethod;
@@ -68,7 +68,7 @@ public abstract class NbLaunchDelegate {
         // no op.
     }
 
-    public final CompletableFuture<Void> nbLaunch(FileObject toRun, String 
method, DebugAdapterContext context, boolean debug, 
Consumer<NbProcessConsole.ConsoleMessage> consoleMessages) {
+    public final CompletableFuture<Void> nbLaunch(FileObject toRun, String 
method, DebugAdapterContext context, boolean debug, boolean testRun, 
Consumer<NbProcessConsole.ConsoleMessage> consoleMessages) {
         CompletableFuture<Void> launchFuture = new CompletableFuture<>();
         NbProcessConsole ioContext = new NbProcessConsole(consoleMessages);
         SingleMethod singleMethod;
@@ -77,7 +77,7 @@ public abstract class NbLaunchDelegate {
         } else {
             singleMethod = null;
         }
-        CompletableFuture<Pair<ActionProvider, String>> commandFuture = 
findTargetWithPossibleRebuild(toRun, singleMethod, debug, ioContext);
+        CompletableFuture<Pair<ActionProvider, String>> commandFuture = 
findTargetWithPossibleRebuild(toRun, singleMethod, debug, testRun, ioContext);
         commandFuture.thenAccept((providerAndCommand) -> {
             if (debug) {
                 DebuggerManager.getDebuggerManager().addDebuggerListener(new 
DebuggerManagerAdapter() {
@@ -119,11 +119,6 @@ public abstract class NbLaunchDelegate {
                     notifyFinished(context, success);
                 }
             };
-            Lookup launchCtx = new ProxyLookup(
-                    Lookups.fixed(
-                            toRun, ioContext, progress
-                    ), Lookup.getDefault()
-            );
             OperationContext ctx = OperationContext.find(Lookup.getDefault());
             ctx.addProgressOperationListener(null, new 
ProgressOperationListener() {
                 @Override
@@ -131,6 +126,11 @@ public abstract class NbLaunchDelegate {
                     context.setProcessExecutorHandle(e.getProgressHandle());
                 }
             });
+            TestProgressHandler testProgressHandler = 
ctx.getClient().getNbCodeCapabilities().hasTestResultsSupport() ? new 
TestProgressHandler(ctx.getClient(), Utils.toUri(toRun)) : null;
+            Lookup launchCtx = new ProxyLookup(
+                    testProgressHandler != null ? Lookups.fixed(toRun, 
ioContext, progress, testProgressHandler) : Lookups.fixed(toRun, ioContext, 
progress),
+                    Lookup.getDefault()
+            );
 
             Lookup lookup;
             if (singleMethod != null) {
@@ -149,8 +149,8 @@ public abstract class NbLaunchDelegate {
         return launchFuture;
     }
 
-    private CompletableFuture<Pair<ActionProvider, String>> 
findTargetWithPossibleRebuild(FileObject toRun, SingleMethod singleMethod, 
boolean debug, NbProcessConsole ioContext) throws IllegalArgumentException {
-        Pair<ActionProvider, String> providerAndCommand = findTarget(toRun, 
singleMethod, debug);
+    private CompletableFuture<Pair<ActionProvider, String>> 
findTargetWithPossibleRebuild(FileObject toRun, SingleMethod singleMethod, 
boolean debug, boolean testRun, NbProcessConsole ioContext) throws 
IllegalArgumentException {
+        Pair<ActionProvider, String> providerAndCommand = findTarget(toRun, 
singleMethod, debug, testRun);
         if (providerAndCommand != null) {
             return CompletableFuture.completedFuture(providerAndCommand);
         }
@@ -166,7 +166,7 @@ public abstract class NbLaunchDelegate {
             @Override
             public void finished(boolean success) {
                 if (success) {
-                    Pair<ActionProvider, String> providerAndCommand = 
findTarget(toRun, singleMethod, debug);
+                    Pair<ActionProvider, String> providerAndCommand = 
findTarget(toRun, singleMethod, debug, testRun);
                     if (providerAndCommand != null) {
                         afterBuild.complete(providerAndCommand);
                         return;
@@ -199,14 +199,14 @@ public abstract class NbLaunchDelegate {
         return afterBuild;
     }
 
-    protected static @CheckForNull Pair<ActionProvider, String> 
findTarget(FileObject toRun, SingleMethod singleMethod, boolean debug) {
+    protected static @CheckForNull Pair<ActionProvider, String> 
findTarget(FileObject toRun, SingleMethod singleMethod, boolean debug, boolean 
testRun) {
         ClassPath sourceCP = ClassPath.getClassPath(toRun, ClassPath.SOURCE);
         FileObject fileRoot = sourceCP != null ? sourceCP.findOwnerRoot(toRun) 
: null;
         boolean mainSource;
         if (fileRoot != null) {
             mainSource = UnitTestForSourceQuery.findUnitTests(fileRoot).length 
> 0;
         } else {
-            mainSource = true;
+            mainSource = !testRun;
         }
         ActionProvider provider = null;
         String command = null;
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
index 66ab8d9..2ed7418 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
@@ -124,7 +124,8 @@ public final class NbLaunchRequestHandler {
             context.setSourcePaths((String[]) 
launchArguments.get("sourcePaths"));
         }
         String singleMethod = (String)launchArguments.get("singleMethod");
-        activeLaunchHandler.nbLaunch(file, singleMethod, context, !noDebug, 
new OutputListener(context)).thenRun(() -> {
+        boolean testRun = (Boolean) launchArguments.getOrDefault("testRun", 
Boolean.FALSE);
+        activeLaunchHandler.nbLaunch(file, singleMethod, context, !noDebug, 
testRun, new OutputListener(context)).thenRun(() -> {
             activeLaunchHandler.postLaunch(launchArguments, context);
             resultFuture.complete(null);
         }).exceptionally(e -> {
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java
new file mode 100644
index 0000000..2818905
--- /dev/null
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.progress;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.netbeans.api.extexecution.print.LineConvertors;
+import org.netbeans.modules.gsf.testrunner.api.Report;
+import org.netbeans.modules.gsf.testrunner.api.TestSession;
+import org.netbeans.modules.gsf.testrunner.api.TestSuite;
+import org.netbeans.modules.gsf.testrunner.ui.api.TestResultDisplayHandler;
+import org.netbeans.modules.java.lsp.server.Utils;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.java.lsp.server.protocol.TestProgressParams;
+import org.netbeans.modules.java.lsp.server.protocol.TestSuiteInfo;
+import org.openide.filesystems.FileObject;
+
+/**
+ *
+ * @author Dusan Balek
+ */
+public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<TestProgressHandler> {
+
+    private final NbCodeLanguageClient client;
+    private final String uri;
+
+    public TestProgressHandler(NbCodeLanguageClient client, String uri) {
+        this.client = client;
+        this.uri = uri;
+    }
+
+    @Override
+    public TestProgressHandler create(TestSession session) {
+        return this;
+    }
+
+    @Override
+    public void displayOutput(TestProgressHandler token, String text, boolean 
error) {
+    }
+
+    @Override
+    public void displaySuiteRunning(TestProgressHandler token, String 
suiteName) {
+        client.notifyTestProgress(new TestProgressParams(uri, new 
TestSuiteInfo(suiteName, TestSuiteInfo.State.Running)));
+    }
+
+    @Override
+    public void displaySuiteRunning(TestProgressHandler token, TestSuite 
suite) {
+        client.notifyTestProgress(new TestProgressParams(uri, new 
TestSuiteInfo(suite.getName(), TestSuiteInfo.State.Running)));
+    }
+
+    @Override
+    public void displayReport(TestProgressHandler token, Report report) {
+        Map<String, FileObject> fileLocations = new HashMap<>();
+        List<TestSuiteInfo.TestCaseInfo> tests = 
report.getTests().stream().map((test) -> {
+            String className = test.getClassName();
+            String displayName = test.getDisplayName();
+            String shortName = displayName.startsWith(className + '.') ? 
displayName.substring(className.length() + 1) : displayName;
+            int idx = shortName.indexOf('(');
+            if (idx > 0) {
+                shortName = shortName.substring(0, idx);
+            }
+            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());
+            }
+            List<String> stackTrace = test.getTrouble() != null ? 
Arrays.asList(test.getTrouble().getStackTrace()) : null;
+            String location = test.getLocation();
+            FileObject fo = location != null ? 
fileLocations.computeIfAbsent(location, loc -> {
+                LineConvertors.FileLocator fileLocator = 
test.getSession().getProject().getLookup().lookup(LineConvertors.FileLocator.class);
+                int i = loc.indexOf(':');
+                if (i > 0) {
+                    loc = loc.substring(0, i);
+                }
+                return fileLocator != null ? fileLocator.find(loc) : null;
+            }) : null;
+            return new TestSuiteInfo.TestCaseInfo(className + ':' + shortName, 
shortName, displayName,
+                    fo != null ? Utils.toUri(fo) : null, null, status, 
stackTrace);
+        }).collect(Collectors.toList());
+        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());
+        }
+        FileObject fo = fileLocations.size() == 1 ? 
fileLocations.values().iterator().next() : null;
+        client.notifyTestProgress(new TestProgressParams(uri, new 
TestSuiteInfo(report.getSuiteClassName(),
+                fo != null ? Utils.toUri(fo) : null, null, status, tests)));
+    }
+
+    @Override
+    public void displayMessage(TestProgressHandler token, String message) {
+    }
+
+    @Override
+    public void displayMessageSessionFinished(TestProgressHandler token, 
String message) {
+    }
+
+    @Override
+    public int getTotalTests(TestProgressHandler token) {
+        return 0;
+    }
+}
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java
index 33046d3..60a83ea 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java
@@ -47,7 +47,15 @@ public final class NbCodeClientCapabilities {
      * </ul>
      */
     private Boolean statusBarMessageSupport;
-    
+
+    /**
+     * Supports test results display:
+     * <ul>
+     * <li>window/notifyTestProgress
+     * </ul>
+     */
+    private Boolean testResultsSupport;
+
     public ClientCapabilities getClientCapabilities() {
         return clientCaps;
     }
@@ -63,7 +71,19 @@ public final class NbCodeClientCapabilities {
     public void setStatusBarMessageSupport(Boolean statusBarMessageSupport) {
         this.statusBarMessageSupport = statusBarMessageSupport;
     }
-    
+
+    public Boolean getTestResultsSupport() {
+        return testResultsSupport;
+    }
+
+    public boolean hasTestResultsSupport() {
+        return testResultsSupport != null && testResultsSupport.booleanValue();
+    }
+
+    public void setTestResultsSupport(Boolean testResultsSupport) {
+        this.testResultsSupport = testResultsSupport;
+    }
+
     private NbCodeClientCapabilities withCapabilities(ClientCapabilities caps) 
{
         if (caps == null) {
             caps = new ClientCapabilities();
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java
index 975f018..0d4f513 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientWrapper.java
@@ -76,6 +76,11 @@ class NbCodeClientWrapper implements NbCodeLanguageClient {
     }
 
     @Override
+    public void notifyTestProgress(TestProgressParams params) {
+        remote.notifyTestProgress(params);
+    }
+
+    @Override
     public CompletableFuture<ApplyWorkspaceEditResponse> 
applyEdit(ApplyWorkspaceEditParams params) {
         return remote.applyEdit(params);
     }
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
index 50fe926..6954b40 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
@@ -63,6 +63,15 @@ public interface NbCodeLanguageClient extends LanguageClient 
{
     public CompletableFuture<String> showInputBox(@NonNull ShowInputBoxParams 
params);
 
     /**
+     * Notifies client of running tests progress. Provides information about a 
test suite being loaded,
+     * started, completed or skipped during a test run.
+     *
+     * @param params test run information
+     */
+    @JsonNotification("window/notifyTestProgress")
+    public void notifyTestProgress(@NonNull TestProgressParams params);
+
+    /**
      * Returns extended code capabilities.
      * @return code capabilities.
      */
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/QuickPickItem.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/QuickPickItem.java
index 3b17385..633c703 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/QuickPickItem.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/QuickPickItem.java
@@ -86,7 +86,7 @@ public class QuickPickItem {
      * A human-readable string which is rendered prominent.
      */
     public void setLabel(@NonNull final String label) {
-        this.label = label;
+        this.label = Preconditions.checkNotNull(label, "label");
     }
 
     /**
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
index d2b0251..15e2b62 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
@@ -225,7 +225,7 @@ public final class Server {
     private static final RequestProcessor SERVER_INIT_RP = new 
RequestProcessor(LanguageServerImpl.class.getName());
     
     
-    private static class LanguageServerImpl implements LanguageServer, 
LanguageClientAware {
+    static class LanguageServerImpl implements LanguageServer, 
LanguageClientAware {
 
         private static final Logger LOG = 
Logger.getLogger(LanguageServerImpl.class.getName());
         private NbCodeClientWrapper client;
@@ -241,7 +241,7 @@ public final class Server {
             return sessionLookup;
         }
         
-        private void asyncOpenSelectedProjects(CompletableFuture f, 
List<FileObject> projectCandidates) {
+        void asyncOpenSelectedProjects(CompletableFuture f, List<FileObject> 
projectCandidates) {
             List<Project> projects = new ArrayList<>();
             try {
                 for (FileObject candidate : projectCandidates) {
@@ -323,7 +323,7 @@ public final class Server {
                 capabilities.setDocumentHighlightProvider(true);
                 capabilities.setReferencesProvider(true);
                 List<String> commands = new ArrayList<>(Arrays.asList(
-                        JAVA_BUILD_WORKSPACE, GRAALVM_PAUSE_SCRIPT, 
JAVA_SUPER_IMPLEMENTATION));
+                        JAVA_BUILD_WORKSPACE, JAVA_LOAD_WORKSPACE_TESTS, 
GRAALVM_PAUSE_SCRIPT, JAVA_SUPER_IMPLEMENTATION));
                 for (CodeGenerator codeGenerator : 
Lookup.getDefault().lookupAll(CodeGenerator.class)) {
                     commands.addAll(codeGenerator.getCommands());
                 }
@@ -419,6 +419,7 @@ public final class Server {
     }
     
     public static final String JAVA_BUILD_WORKSPACE =  "java.build.workspace";
+    public static final String JAVA_LOAD_WORKSPACE_TESTS =  
"java.load.workspace.tests";
     public static final String JAVA_SUPER_IMPLEMENTATION =  
"java.super.implementation";
     public static final String GRAALVM_PAUSE_SCRIPT =  "graalvm.pause.script";
     static final String INDEXING_COMPLETED = "Indexing completed.";
@@ -450,6 +451,11 @@ public final class Server {
         }
 
         @Override
+        public void notifyTestProgress(TestProgressParams params) {
+            logWarning(params);
+        }
+
+        @Override
         public NbCodeClientCapabilities getNbCodeCapabilities() {
             logWarning();
             return caps;
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
index a262919..68ad7a0 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
@@ -65,7 +65,7 @@ public class ShowInputBoxParams {
      * The text to display underneath the input box.
      */
     public void setPrompt(@NonNull final String prompt) {
-        this.prompt = prompt;
+        this.prompt = Preconditions.checkNotNull(prompt, "prompt");
     }
 
     /**
@@ -81,7 +81,7 @@ public class ShowInputBoxParams {
      * The value to prefill in the input box.
      */
     public void setValue(@NonNull final String value) {
-        this.value = value;
+        this.value = Preconditions.checkNotNull(value, "value");
     }
 
     @Override
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowQuickPickParams.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowQuickPickParams.java
index 107084f..5046329 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowQuickPickParams.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowQuickPickParams.java
@@ -78,7 +78,7 @@ public class ShowQuickPickParams {
      * A string to show as placeholder in the input box to guide the user what 
to pick on.
      */
     public void setPlaceHolder(@NonNull final String placeHolder) {
-        this.placeHolder = placeHolder;
+        this.placeHolder = Preconditions.checkNotNull(placeHolder, 
"placeHolder");
     }
 
     /**
@@ -109,7 +109,7 @@ public class ShowQuickPickParams {
      * A list of items.
      */
     public void setItems(@NonNull final List<QuickPickItem> items) {
-        this.items = items;
+        this.items = Preconditions.checkNotNull(items, "items");
     }
 
     @Override
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestProgressParams.java
similarity index 54%
copy from 
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
copy to 
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestProgressParams.java
index a262919..21550fe 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ShowInputBoxParams.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestProgressParams.java
@@ -29,79 +29,84 @@ import org.eclipse.xtext.xbase.lib.util.ToStringBuilder;
  * @author Dusan Balek
  */
 @SuppressWarnings("all")
-public class ShowInputBoxParams {
+public class TestProgressParams {
 
     /**
-     * The text to display underneath the input box.
+     * The test suite or the workspace folder the test suite belongs to.
      */
     @NonNull
-    private String prompt;
+    private String uri;
 
     /**
-     * The value to prefill in the input box.
+     * Information about a test suite being loaded, started, completed or 
skipped
+     * during a test run.
      */
     @NonNull
-    private String value;
+    private TestSuiteInfo suite;
 
-    public ShowInputBoxParams() {
-        this("", "");
+    public TestProgressParams() {
+        this("", new TestSuiteInfo());
     }
 
-    public ShowInputBoxParams(@NonNull final String prompt, @NonNull final 
String value) {
-        this.prompt = Preconditions.checkNotNull(prompt, "prompt");
-        this.value = Preconditions.checkNotNull(value, "value");
+    public TestProgressParams(@NonNull final String uri, @NonNull final 
TestSuiteInfo suite) {
+        this.uri = Preconditions.checkNotNull(uri, "uri");
+        this.suite = Preconditions.checkNotNull(suite, "suite");
     }
 
     /**
-     * The text to display underneath the input box.
+     * The test suite or the workspace folder the test suite belongs to.
      */
     @Pure
     @NonNull
-    public String getPrompt() {
-        return prompt;
+    public String getUri() {
+        return uri;
     }
 
     /**
-     * The text to display underneath the input box.
+     * The test suite or the workspace folder the test suite belongs to.
      */
-    public void setPrompt(@NonNull final String prompt) {
-        this.prompt = prompt;
+    public void setUri(@NonNull final String uri) {
+        this.uri = Preconditions.checkNotNull(uri, "uri");
     }
 
     /**
-     * The value to prefill in the input box.
+     * Information about a test suite being loaded, started, completed or 
skipped
+     * during a test run.
      */
     @Pure
     @NonNull
-    public String getValue() {
-        return value;
+    public TestSuiteInfo getSuite() {
+        return suite;
     }
 
     /**
-     * The value to prefill in the input box.
+     * Information about a test suite being loaded, started, completed or 
skipped
+     * during a test run.
      */
-    public void setValue(@NonNull final String value) {
-        this.value = value;
+    public void setSuite(@NonNull final TestSuiteInfo suite) {
+        this.suite = Preconditions.checkNotNull(suite, "suite");
     }
 
     @Override
     @Pure
     public String toString() {
         ToStringBuilder b = new ToStringBuilder(this);
-        b.add("prompt", prompt);
-        b.add("value", value);
+        b.add("uri", uri);
+        b.add("suite", suite);
         return b.toString();
     }
 
     @Override
+    @Pure
     public int hashCode() {
         int hash = 5;
-        hash = 59 * hash + Objects.hashCode(this.prompt);
-        hash = 59 * hash + Objects.hashCode(this.value);
+        hash = 59 * hash + Objects.hashCode(this.uri);
+        hash = 59 * hash + Objects.hashCode(this.suite);
         return hash;
     }
 
     @Override
+    @Pure
     public boolean equals(Object obj) {
         if (this == obj) {
             return true;
@@ -112,11 +117,11 @@ public class ShowInputBoxParams {
         if (getClass() != obj.getClass()) {
             return false;
         }
-        final ShowInputBoxParams other = (ShowInputBoxParams) obj;
-        if (!Objects.equals(this.prompt, other.prompt)) {
+        final TestProgressParams other = (TestProgressParams) obj;
+        if (!Objects.equals(this.uri, other.uri)) {
             return false;
         }
-        if (!Objects.equals(this.value, other.value)) {
+        if (!Objects.equals(this.suite, other.suite)) {
             return false;
         }
         return true;
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
new file mode 100644
index 0000000..d92337d
--- /dev/null
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestSuiteInfo.java
@@ -0,0 +1,477 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.protocol;
+
+import java.util.List;
+import java.util.Objects;
+import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
+import org.eclipse.lsp4j.util.Preconditions;
+import org.eclipse.xtext.xbase.lib.Pure;
+import org.eclipse.xtext.xbase.lib.util.ToStringBuilder;
+
+/**
+ * Information about a test suite.
+ *
+ * @author Dusan Balek
+ */
+public final class TestSuiteInfo {
+
+    /**
+     * The test suite name to be displayed by the Test Explorer.
+     */
+    @NonNull
+    private String suiteName;
+
+    /**
+     * The file containing this suite (if known).
+     */
+    private String file;
+
+    /**
+     * The line within the specified file where the suite definition starts 
(if known).
+     */
+    private Integer line;
+
+    /**
+     * The state of the tests suite. Can be one of the following values:
+     * "loaded" | "running" | "completed" | "errored"
+     */
+    @NonNull
+    private String state;
+
+    /**
+     * The test cases of the test suite.
+     */
+    private List<TestCaseInfo> tests;
+
+    public TestSuiteInfo() {
+        this("", "");
+    }
+
+    public TestSuiteInfo(@NonNull final String suiteName, @NonNull final 
String state) {
+        this.suiteName = Preconditions.checkNotNull(suiteName, "suiteName");
+        this.state = Preconditions.checkNotNull(state, "state");
+    }
+
+    public TestSuiteInfo(@NonNull final String suiteName, final String file, 
final Integer line, @NonNull final String state, final List<TestCaseInfo> 
tests) {
+        this(suiteName, state);
+        this.file = file;
+        this.line = line;
+        this.tests = tests;
+    }
+
+    /**
+     * The test suite name to be displayed by the Test Explorer.
+     */
+    @Pure
+    @NonNull
+    public String getSuiteName() {
+        return suiteName;
+    }
+
+    /**
+     * The test suite name to be displayed by the Test Explorer.
+     */
+    public void setSuiteName(@NonNull final String suiteName) {
+        this.suiteName = Preconditions.checkNotNull(suiteName, "suiteName");
+    }
+
+    /**
+     * The file containing this suite (if known).
+     */
+    @Pure
+    public String getFile() {
+        return file;
+    }
+
+    /**
+     * The file containing this suite (if known).
+     */
+    public void setFile(final String file) {
+        this.file = file;
+    }
+
+    /**
+     * The line within the specified file where the suite definition starts 
(if known).
+     */
+    @Pure
+    public Integer getLine() {
+        return line;
+    }
+
+    /**
+     * The line within the specified file where the suite definition starts 
(if known).
+     */
+    public void setLine(final Integer line) {
+        this.line = line;
+    }
+
+    /**
+     * The state of the tests suite. Can be one of the following values:
+     * "loaded" | "running" | "completed" | "errored"
+     */
+    @Pure
+    @NonNull
+    public String getState() {
+        return state;
+    }
+
+    /**
+     * The state of the tests suite. Can be one of the following values:
+     * "loaded" | "running" | "completed" | "errored"
+     */
+    public void setState(@NonNull final String state) {
+        this.state = Preconditions.checkNotNull(state, "state");
+    }
+
+    /**
+     * The test cases of the test suite.
+     */
+    @Pure
+    public List<TestCaseInfo> getTests() {
+        return tests;
+    }
+
+    /**
+     * The test cases of the test suite.
+     */
+    public void setTests(List<TestCaseInfo> tests) {
+        this.tests = tests;
+    }
+
+    @Override
+    @Pure
+    public String toString() {
+        ToStringBuilder b = new ToStringBuilder(this);
+        b.add("suiteName", suiteName);
+        b.add("file", file);
+        b.add("line", line);
+        b.add("state", state);
+        b.add("tests", tests);
+        return b.toString();
+    }
+
+    @Override
+    @Pure
+    public int hashCode() {
+        int hash = 7;
+        hash = 67 * hash + Objects.hashCode(this.suiteName);
+        hash = 67 * hash + Objects.hashCode(this.file);
+        hash = 67 * hash + Objects.hashCode(this.line);
+        hash = 67 * hash + Objects.hashCode(this.state);
+        hash = 67 * hash + Objects.hashCode(this.tests);
+        return hash;
+    }
+
+    @Override
+    @Pure
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TestSuiteInfo other = (TestSuiteInfo) obj;
+        if (!Objects.equals(this.suiteName, other.suiteName)) {
+            return false;
+        }
+        if (!Objects.equals(this.file, other.file)) {
+            return false;
+        }
+        if (!Objects.equals(this.line, other.line)) {
+            return false;
+        }
+        if (!Objects.equals(this.state, other.state)) {
+            return false;
+        }
+        if (!Objects.equals(this.tests, other.tests)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Information about a test case.
+     */
+    public static class TestCaseInfo {
+
+        /**
+         * The test case ID.
+         */
+        @NonNull
+        private String id;
+
+        /**
+         * The short name to be displayed by the Test Explorer for this test 
case.
+         */
+        @NonNull
+        private String shortName;
+
+        /**
+         * The full name to be displayed by the Test Explorer when you hover 
over
+         * this test case.
+         */
+        @NonNull
+        private String fullName;
+
+        /**
+         * The file containing this test case (if known).
+         */
+        private String file;
+
+        /**
+         * The line within the specified file where the test case definition 
starts (if known).
+         */
+        private Integer line;
+
+        /**
+         * The state of the test case. Can be one of the following values:
+         * "loaded" | "running" | "passed" | "failed" | "skipped" | "errored"
+         */
+        @NonNull
+        private String state;
+
+        /**
+         * Stack trace for a test failure.
+         */
+        private List<String> stackTrace;
+
+        public TestCaseInfo() {
+            this("", "", "", "");
+        }
+
+        public TestCaseInfo(@NonNull final String id, @NonNull final String 
shortName, @NonNull final String fullName, @NonNull final String state) {
+            this.id = Preconditions.checkNotNull(id, "id");
+            this.shortName = Preconditions.checkNotNull(shortName, 
"shortName");
+            this.fullName = Preconditions.checkNotNull(fullName, "fullName");
+            this.state = Preconditions.checkNotNull(state, "state");
+        }
+
+        public TestCaseInfo(@NonNull final String id, @NonNull final String 
shortName, @NonNull final String fullName, final String file, final Integer 
line, @NonNull final String state, final List<String> stackTrace) {
+            this(id, shortName, fullName, state);
+            this.file = file;
+            this.line = line;
+            this.stackTrace = stackTrace;
+        }
+
+        /**
+         * The test case ID.
+         */
+        @Pure
+        @NonNull
+        public String getId() {
+            return id;
+        }
+
+        /**
+         * The test case ID.
+         */
+        public void setId(@NonNull final String id) {
+            this.id = Preconditions.checkNotNull(id, "id");
+        }
+
+        /**
+         * The short name to be displayed by the Test Explorer for this test 
case.
+         */
+        @Pure
+        @NonNull
+        public String getShortName() {
+            return shortName;
+        }
+
+        /**
+         * The short name to be displayed by the Test Explorer for this test 
case.
+         */
+        public void setShortName(@NonNull final String shortName) {
+            this.shortName = Preconditions.checkNotNull(shortName, 
"shortName");
+        }
+
+        /**
+         * The full name to be displayed by the Test Explorer when you hover 
over
+         * this test case.
+         */
+        @Pure
+        @NonNull
+        public String getFullName() {
+            return fullName;
+        }
+
+        /**
+         * The full name to be displayed by the Test Explorer when you hover 
over
+         * this test case.
+         */
+        public void setFullName(@NonNull final String fullName) {
+            this.fullName = Preconditions.checkNotNull(fullName, "fullName");
+        }
+
+        /**
+         * The file containing this test case (if known).
+         */
+        @Pure
+        public String getFile() {
+            return file;
+        }
+
+        /**
+         * The file containing this test case (if known).
+         */
+        public void setFile(final String file) {
+            this.file = file;
+        }
+
+        /**
+         * The line within the specified file where the test case definition 
starts (if known).
+         */
+        @Pure
+        public Integer getLine() {
+            return line;
+        }
+
+        /**
+         * The line within the specified file where the test case definition 
starts (if known).
+         */
+        public void setLine(final Integer line) {
+            this.line = line;
+        }
+
+        /**
+         * The state of the test case. Can be one of the following values:
+         * "loaded" | "running" | "passed" | "failed" | "skipped" | "errored"
+         */
+        @Pure
+        @NonNull
+        public String getState() {
+            return state;
+        }
+
+        /**
+         * The state of the test case. Can be one of the following values:
+         * "loaded" | "running" | "passed" | "failed" | "skipped" | "errored"
+         */
+        public void setState(@NonNull final String state) {
+            this.state = Preconditions.checkNotNull(state, "state");
+        }
+
+        /**
+         * Stack trace for a test failure.
+         */
+        @Pure
+        public List<String> getStackTrace() {
+            return stackTrace;
+        }
+
+        /**
+         * Stack trace for a test failure.
+         */
+        public void setStackTrace(final List<String> stackTrace) {
+            this.stackTrace = stackTrace;
+        }
+
+        @Override
+        @Pure
+        public String toString() {
+            ToStringBuilder b = new ToStringBuilder(this);
+            b.add("id", id);
+            b.add("shortName", shortName);
+            b.add("fullName", fullName);
+            b.add("file", file);
+            b.add("line", line);
+            b.add("state", state);
+            b.add("stackTrace", stackTrace);
+            return b.toString();
+        }
+
+        @Override
+        @Pure
+        public int hashCode() {
+            int hash = 5;
+            hash = 97 * hash + Objects.hashCode(this.id);
+            hash = 97 * hash + Objects.hashCode(this.shortName);
+            hash = 97 * hash + Objects.hashCode(this.fullName);
+            hash = 97 * hash + Objects.hashCode(this.file);
+            hash = 97 * hash + Objects.hashCode(this.line);
+            hash = 97 * hash + Objects.hashCode(this.state);
+            hash = 97 * hash + Objects.hashCode(this.stackTrace);
+            return hash;
+        }
+
+        @Override
+        @Pure
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final TestCaseInfo other = (TestCaseInfo) obj;
+            if (!Objects.equals(this.id, other.id)) {
+                return false;
+            }
+            if (!Objects.equals(this.shortName, other.shortName)) {
+                return false;
+            }
+            if (!Objects.equals(this.fullName, other.fullName)) {
+                return false;
+            }
+            if (!Objects.equals(this.file, other.file)) {
+                return false;
+            }
+            if (!Objects.equals(this.line, other.line)) {
+                return false;
+            }
+            if (!Objects.equals(this.state, other.state)) {
+                return false;
+            }
+            if (!Objects.equals(this.stackTrace, other.stackTrace)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Constants for test states.
+     */
+    public static final class State {
+
+        private State() {}
+
+        public static final String Loaded = "loaded";
+
+        public static final String Running = "running";
+
+        public static final String Completed  = "completed";
+
+        public static final String Passed  = "passed";
+
+        public static final String Failed  = "failed";
+
+        public static final String Skipped  = "skipped";
+
+        public static final String Errored  = "errored";
+    }
+}
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
index 6f4d889..dc88000 100644
--- 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
+++ 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
@@ -234,7 +234,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
     private final Map<String, RequestProcessor.Task> diagnosticTasks = new 
HashMap<>();
     private NbCodeLanguageClient client;
 
-    public TextDocumentServiceImpl() {
+    TextDocumentServiceImpl() {
         Lookup.getDefault().lookup(RefreshDocument.class).register(this);
     }
 
@@ -830,7 +830,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
     @Override
     public CompletableFuture<Either<List<? extends Location>, List<? extends 
LocationLink>>> definition(DefinitionParams params) {
         String uri = params.getTextDocument().getUri();
-        JavaSource js = getSource(uri);
+        JavaSource js = getJavaSource(uri);
         GoToTarget[] target = new GoToTarget[1];
         LineMap[] thisFileLineMap = new LineMap[1];
         try {
@@ -855,7 +855,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
     @Override
     public CompletableFuture<Either<List<? extends Location>, List<? extends 
LocationLink>>> implementation(ImplementationParams params) {
         String uri = params.getTextDocument().getUri();
-        JavaSource js = getSource(uri);
+        JavaSource js = getJavaSource(uri);
         List<GoToTarget> targets = new ArrayList<>();
         LineMap[] thisFileLineMap = new LineMap[1];
         try {
@@ -944,7 +944,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
             }
         };
         WORKER.post(() -> {
-            JavaSource js = getSource(params.getTextDocument().getUri());
+            JavaSource js = getJavaSource(params.getTextDocument().getUri());
             try {
                 WhereUsedQuery[] query = new WhereUsedQuery[1];
                 List<Location> locations = new ArrayList<>();
@@ -1066,7 +1066,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
 
         Preferences node = MarkOccurencesSettings.getCurrentNode();
 
-        JavaSource js = getSource(params.getTextDocument().getUri());
+        JavaSource js = getJavaSource(params.getTextDocument().getUri());
         List<DocumentHighlight> result = new ArrayList<>();
         try {
             js.runUserActionTask(cc -> {
@@ -1090,7 +1090,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
 
     @Override
     public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> 
documentSymbol(DocumentSymbolParams params) {
-        JavaSource js = getSource(params.getTextDocument().getUri());
+        JavaSource js = getJavaSource(params.getTextDocument().getUri());
         List<Either<SymbolInformation, DocumentSymbol>> result = new 
ArrayList<>();
         try {
             js.runUserActionTask(cc -> {
@@ -1373,7 +1373,8 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
 
     @Override
     public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams 
params) {
-        JavaSource source = getSource(params.getTextDocument().getUri());
+        String uri = params.getTextDocument().getUri();
+        JavaSource source = getJavaSource(uri);
         if (source == null) {
             return CompletableFuture.completedFuture(Collections.emptyList());
         }
@@ -1381,25 +1382,36 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
         try {
             source.runUserActionTask(cc -> {
                 cc.toPhase(Phase.ELEMENTS_RESOLVED);
-                List<CodeLens> lens = new ArrayList<>();
                 //look for test methods:
+                List<TestMethod> testMethods = new ArrayList<>();
                 for (ComputeTestMethods.Factory methodsFactory : 
Lookup.getDefault().lookupAll(ComputeTestMethods.Factory.class)) {
-                    List<TestMethod> methods = 
methodsFactory.create().computeTestMethods(cc);
-                    if (methods != null) {
-                        for (TestMethod method : methods) {
-                            Range range = new 
Range(Utils.createPosition(cc.getCompilationUnit(), method.start().getOffset()),
-                                                    
Utils.createPosition(cc.getCompilationUnit(), method.end().getOffset()));
-                            List<Object> arguments = Arrays.asList(new 
Object[]{method.method().getFile().toURI(), method.method().getMethodName()});
-                            lens.add(new CodeLens(range,
-                                                  new Command("Run test", 
"java.run.codelens", arguments),
-                                                  null));
-                            lens.add(new CodeLens(range,
-                                                  new Command("Debug test", 
"java.debug.codelens", arguments),
-                                                  null));
+                    
testMethods.addAll(methodsFactory.create().computeTestMethods(cc));
+                }
+                if (!testMethods.isEmpty()) {
+                    String testClassName = null;
+                    List<TestSuiteInfo.TestCaseInfo> tests = new 
ArrayList<>(testMethods.size());
+                    for (TestMethod testMethod : testMethods) {
+                        if (testClassName == null) {
+                            testClassName = testMethod.getTestClassName();
+                        }
+                        String id = testMethod.getTestClassName() + ':' + 
testMethod.method().getMethodName();
+                        String fullName = testMethod.getTestClassName() + '.' 
+ testMethod.method().getMethodName();
+                        int line = 
Utils.createPosition(cc.getCompilationUnit(), 
testMethod.start().getOffset()).getLine();
+                        tests.add(new TestSuiteInfo.TestCaseInfo(id, 
testMethod.method().getMethodName(), fullName, uri, line, 
TestSuiteInfo.State.Loaded, null));
+                    }
+                    Integer line = null;
+                    Trees trees = cc.getTrees();
+                    for (Tree tree : cc.getCompilationUnit().getTypeDecls()) {
+                        Element element = 
trees.getElement(trees.getPath(cc.getCompilationUnit(), tree));
+                        if (element != null && element.getKind().isClass() && 
((TypeElement)element).getQualifiedName().contentEquals(testClassName)) {
+                            line = 
Utils.createPosition(cc.getCompilationUnit(), 
(int)trees.getSourcePositions().getStartPosition(cc.getCompilationUnit(), 
tree)).getLine();
+                            break;
                         }
                     }
+                    client.notifyTestProgress(new TestProgressParams(uri, new 
TestSuiteInfo(testClassName, uri, line, TestSuiteInfo.State.Loaded, tests)));
                 }
                 //look for main methods:
+                List<CodeLens> lens = new ArrayList<>();
                 new TreePathScanner<Void, Void>() {
                     public Void visitMethod(MethodTree tree, Void p) {
                         Element el = 
cc.getTrees().getElement(getCurrentPath());
@@ -1407,10 +1419,10 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
                             Range range = Utils.treeRange(cc, tree);
                             List<Object> arguments = 
Collections.singletonList(params.getTextDocument().getUri());
                             lens.add(new CodeLens(range,
-                                                  new Command("Run main", 
"java.run.codelens", arguments),
+                                                  new Command("Run main", 
"java.run.single", arguments),
                                                   null));
                             lens.add(new CodeLens(range,
-                                                  new Command("Debug main", 
"java.debug.codelens", arguments),
+                                                  new Command("Debug main", 
"java.debug.single", arguments),
                                                   null));
                         }
                         return null;
@@ -1446,7 +1458,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
 
     @Override
     public CompletableFuture<Either<Range, PrepareRenameResult>> 
prepareRename(PrepareRenameParams params) {
-        JavaSource source = getSource(params.getTextDocument().getUri());
+        JavaSource source = getJavaSource(params.getTextDocument().getUri());
         if (source == null) {
             return CompletableFuture.completedFuture(Either.forLeft(null));
         }
@@ -1504,7 +1516,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
             }
         };
         WORKER.post(() -> {
-            JavaSource js = getSource(params.getTextDocument().getUri());
+            JavaSource js = getJavaSource(params.getTextDocument().getUri());
             try {
                 RenameRefactoring[] refactoring = new RenameRefactoring[1];
                 js.runUserActionTask(cc -> {
@@ -1603,7 +1615,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
 
     @Override
     public CompletableFuture<List<FoldingRange>> 
foldingRange(FoldingRangeRequestParams params) {
-        JavaSource source = getSource(params.getTextDocument().getUri());
+        JavaSource source = getJavaSource(params.getTextDocument().getUri());
         if (source == null) {
             return CompletableFuture.completedFuture(Collections.emptyList());
         }
@@ -1730,7 +1742,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
     }
 
     CompletableFuture<Location> superImplementation(String uri, Position 
position) {
-        JavaSource js = getSource(uri);
+        JavaSource js = getJavaSource(uri);
         GoToTarget[] target = new GoToTarget[1];
         LineMap[] thisFileLineMap = new LineMap[1];
         try {
@@ -1952,7 +1964,7 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
         public List<ErrorDescription> computeErrors(CompilationInfo info, 
Document doc) throws IOException;
     }
 
-    private JavaSource getSource(String fileUri) {
+    public JavaSource getJavaSource(String fileUri) {
         Document doc = openedDocuments.get(fileUri);
         if (doc == null) {
             try {
@@ -1966,6 +1978,20 @@ public class TextDocumentServiceImpl implements 
TextDocumentService, LanguageCli
         }
     }
 
+    public Source getSource(String fileUri) {
+        Document doc = openedDocuments.get(fileUri);
+        if (doc == null) {
+            try {
+                FileObject file = Utils.fromUri(fileUri);
+                return Source.create(file);
+            } catch (MalformedURLException ex) {
+                return null;
+            }
+        } else {
+            return Source.create(doc);
+        }
+    }
+
     public static List<TextEdit> modify2TextEdits(JavaSource js, 
Task<WorkingCopy> task) throws IOException {
         FileObject[] file = new FileObject[1];
         LineMap[] lm = new LineMap[1];
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 587624f..80305fe 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
@@ -20,17 +20,23 @@ package org.netbeans.modules.java.lsp.server.protocol;
 
 import com.google.gson.Gson;
 import com.google.gson.JsonPrimitive;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.LineMap;
+import com.sun.source.tree.Tree;
 import com.sun.source.util.TreePath;
+import com.sun.source.util.Trees;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
 import javax.lang.model.element.Element;
@@ -45,38 +51,44 @@ import org.eclipse.lsp4j.SymbolInformation;
 import org.eclipse.lsp4j.WorkspaceSymbolParams;
 import org.eclipse.lsp4j.services.LanguageClient;
 import org.eclipse.lsp4j.services.LanguageClientAware;
-import org.eclipse.lsp4j.services.LanguageServer;
 import org.eclipse.lsp4j.services.WorkspaceService;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.annotations.common.NullAllowed;
 import org.netbeans.api.debugger.ActionsManager;
 import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.java.project.JavaProjectConstants;
+import org.netbeans.api.java.queries.UnitTestForSourceQuery;
 import org.netbeans.api.java.source.ClasspathInfo;
+import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.CompilationInfo;
 import org.netbeans.api.java.source.ElementHandle;
 import org.netbeans.api.java.source.JavaSource;
 import org.netbeans.api.java.source.JavaSource.Phase;
 import org.netbeans.api.java.source.SourceUtils;
-import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectUtils;
+import org.netbeans.api.project.SourceGroup;
 import org.netbeans.api.project.ui.OpenProjects;
+import org.netbeans.modules.gsf.testrunner.ui.api.TestMethodController;
 import org.netbeans.modules.java.lsp.server.Utils;
 import org.netbeans.modules.java.source.ui.JavaSymbolProvider;
 import org.netbeans.modules.java.source.ui.JavaSymbolProvider.ResultHandler;
 import 
org.netbeans.modules.java.source.ui.JavaSymbolProvider.ResultHandler.Exec;
 import org.netbeans.modules.java.source.usages.ClassIndexImpl;
+import org.netbeans.modules.java.testrunner.ui.spi.ComputeTestMethods;
+import org.netbeans.modules.parsing.api.ParserManager;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.netbeans.modules.parsing.api.Source;
+import org.netbeans.modules.parsing.api.UserTask;
 import org.netbeans.modules.parsing.lucene.support.Queries;
+import org.netbeans.modules.parsing.spi.ParseException;
 import org.netbeans.spi.jumpto.type.SearchType;
 import org.netbeans.spi.project.ActionProgress;
 import org.netbeans.spi.project.ActionProvider;
-import org.netbeans.spi.project.SingleMethod;
-import org.openide.awt.StatusDisplayer;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.URLMapper;
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
-import org.openide.util.Mutex;
-import org.openide.util.NbBundle;
 import org.openide.util.Pair;
 import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.Lookups;
@@ -90,10 +102,10 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
     private static final RequestProcessor WORKER = new 
RequestProcessor(WorkspaceServiceImpl.class.getName(), 1, false, false);
 
     private final Gson gson = new Gson();
-    private final LanguageServer server;
+    private final Server.LanguageServerImpl server;
     private NbCodeLanguageClient client;
 
-    public WorkspaceServiceImpl(LanguageServer server) {
+    WorkspaceServiceImpl(Server.LanguageServerImpl server) {
         this.server = server;
     }
 
@@ -117,6 +129,46 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
                 progressOfCompilation.checkStatus();
                 return progressOfCompilation.getFinishFuture();
             }
+            case Server.JAVA_LOAD_WORKSPACE_TESTS: {
+                String uri = ((JsonPrimitive) 
params.getArguments().get(0)).getAsString();
+                FileObject file;
+                try {
+                    file = URLMapper.findFileObject(new URL(uri));
+                } catch (MalformedURLException ex) {
+                    Exceptions.printStackTrace(ex);
+                    return CompletableFuture.completedFuture(true);
+                }
+                CompletableFuture<Project[]> projectsFuture = new 
CompletableFuture<>();
+                server.asyncOpenSelectedProjects(projectsFuture, 
Collections.singletonList(file));
+                return projectsFuture.thenApply(projects -> {
+                    List<TestMethodController.TestMethod> testMethods = new 
ArrayList<>();
+                    for (Project prj : projects) {
+                        Set<URL> testRootURLs = new HashSet<>();
+                        for (SourceGroup sg : 
ProjectUtils.getSources(prj).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA))
 {
+                            for (URL url : 
UnitTestForSourceQuery.findUnitTests(sg.getRootFolder())) {
+                                testRootURLs.add(url);
+                            }
+                        }
+                        findTestMethods(testRootURLs, testMethods);
+                    }
+                    if (testMethods.isEmpty()) {
+                        return Collections.emptyList();
+                    }
+                    Map<FileObject, TestSuiteInfo> file2TestSuites = new 
HashMap<>();
+                    for (TestMethodController.TestMethod testMethod : 
testMethods) {
+                        TestSuiteInfo suite = 
file2TestSuites.computeIfAbsent(testMethod.method().getFile(), fo -> {
+                            String foUri = Utils.toUri(fo);
+                            Integer line = 
getTestLine(((TextDocumentServiceImpl)server.getTextDocumentService()).getJavaSource(foUri),
 testMethod.getTestClassName());
+                            return new 
TestSuiteInfo(testMethod.getTestClassName(), foUri, line, 
TestSuiteInfo.State.Loaded, new ArrayList<>());
+                        });
+                        String id = testMethod.getTestClassName() + ':' + 
testMethod.method().getMethodName();
+                        String fullName = testMethod.getTestClassName() + '.' 
+ testMethod.method().getMethodName();
+                        int line = 
Utils.createPosition(testMethod.method().getFile(), 
testMethod.start().getOffset()).getLine();
+                        suite.getTests().add(new 
TestSuiteInfo.TestCaseInfo(id, testMethod.method().getMethodName(), fullName, 
suite.getFile(), line, TestSuiteInfo.State.Loaded, null));
+                    }
+                    return file2TestSuites.values();
+                });
+            }
             case Server.JAVA_SUPER_IMPLEMENTATION:
                 String uri = ((JsonPrimitive) 
params.getArguments().get(0)).getAsString();
                 Position pos = 
gson.fromJson(gson.toJson(params.getArguments().get(1)), Position.class);
@@ -131,6 +183,58 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
         throw new UnsupportedOperationException("Command not supported: " + 
params.getCommand());
     }
 
+    private void findTestMethods(Set<URL> testRootURLs, 
List<TestMethodController.TestMethod> testMethods) {
+        for (URL testRootURL : testRootURLs) {
+            FileObject testRoot = URLMapper.findFileObject(testRootURL);
+            List<Source> sources = new ArrayList<>();
+            Enumeration<? extends FileObject> children = 
testRoot.getChildren(true);
+            while(children.hasMoreElements()) {
+                FileObject fo = children.nextElement();
+                if (fo.hasExt("java")) {
+                    
sources.add(((TextDocumentServiceImpl)server.getTextDocumentService()).getSource(Utils.toUri(fo)));
+                }
+            }
+            if (!sources.isEmpty()) {
+                try {
+                    ParserManager.parse(sources, new UserTask() {
+                        @Override
+                        public void run(ResultIterator resultIterator) throws 
Exception {
+                            CompilationController cc = 
CompilationController.get(resultIterator.getParserResult());
+                            cc.toPhase(Phase.ELEMENTS_RESOLVED);
+                            for (ComputeTestMethods.Factory methodsFactory : 
Lookup.getDefault().lookupAll(ComputeTestMethods.Factory.class)) {
+                                
testMethods.addAll(methodsFactory.create().computeTestMethods(cc));
+                            }
+                        }
+                    });
+                } catch (ParseException ex) {}
+            }
+        }
+    }
+
+    private Integer getTestLine(JavaSource javaSource, String className) {
+        final int[] offset = new int[] {-1};
+        final LineMap[] lm = new LineMap[1];
+        if (javaSource != null) {
+            try {
+                javaSource.runUserActionTask(cc -> {
+                    cc.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
+                    Trees trees = cc.getTrees();
+                    CompilationUnitTree cu = cc.getCompilationUnit();
+                    lm[0] = cu.getLineMap();
+                    for (Tree tree : cu.getTypeDecls()) {
+                        Element element = trees.getElement(trees.getPath(cu, 
tree));
+                        if (element != null && element.getKind().isClass() && 
((TypeElement)element).getQualifiedName().contentEquals(className)) {
+                            offset[0] = 
(int)trees.getSourcePositions().getStartPosition(cu, tree);
+                            return;
+                        }
+                    }
+                }, true);
+            } catch (IOException ioe) {
+            }
+        }
+        return offset[0] < 0 ? null : Utils.createPosition(lm[0], 
offset[0]).getLine();
+    }
+
     @Override
     public CompletableFuture<List<? extends SymbolInformation>> 
symbol(WorkspaceSymbolParams params) {
         String query = params.getQuery();
@@ -329,6 +433,7 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
         @Override
         protected synchronized void started() {
             running++;
+            notify();
         }
 
         @Override
@@ -342,6 +447,12 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
         }
 
         synchronized final void checkStatus() {
+            if (running == 0) {
+                try {
+                    wait(100);
+                } catch (InterruptedException ex) {
+                }
+            }
             if (running <= success + failure) {
                 commandFinished.complete(failure == 0);
             }
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
new file mode 100644
index 0000000..261c80a
--- /dev/null
+++ 
b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandlerTest.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.progress;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.junit.Test;
+import org.netbeans.api.extexecution.print.LineConvertors;
+import org.netbeans.api.project.Project;
+import org.netbeans.junit.NbTestCase;
+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.Testcase;
+import org.netbeans.modules.gsf.testrunner.api.Trouble;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeClientCapabilities;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.java.lsp.server.protocol.QuickPickItem;
+import org.netbeans.modules.java.lsp.server.protocol.ShowInputBoxParams;
+import org.netbeans.modules.java.lsp.server.protocol.ShowQuickPickParams;
+import org.netbeans.modules.java.lsp.server.protocol.ShowStatusMessageParams;
+import org.netbeans.modules.java.lsp.server.protocol.TestProgressParams;
+import org.netbeans.modules.java.lsp.server.protocol.TestSuiteInfo;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.Lookups;
+
+/**
+ *
+ * @author Dusan Balek
+ */
+public class TestProgressHandlerTest extends NbTestCase {
+
+    public TestProgressHandlerTest(String name) {
+        super(name);
+    }
+
+    @Test
+    public void testProgress() {
+        FileObject fo = null;
+        try {
+            fo = FileUtil.toFileObject(getWorkDir());
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        assertNotNull(fo);
+        List<TestProgressParams> msgs = new ArrayList<>();
+        MockLanguageClient mlc = new MockLanguageClient(msgs);
+        TestProgressHandler progressHandler = new TestProgressHandler(mlc, 
fo.toURI().toString());
+        progressHandler.displaySuiteRunning(progressHandler, "TestSuiteName");
+        FileObject projectDir = fo;
+        Project project = new Project() {
+            @Override
+            public FileObject getProjectDirectory() {
+                return projectDir;
+            }
+
+            @Override
+            public Lookup getLookup() {
+                return Lookups.fixed(new LineConvertors.FileLocator() {
+                    @Override
+                    public FileObject find(String filename) {
+                        return "TestSuiteName".equals(filename) ? projectDir : 
null;
+                    }
+                });
+            }
+        };
+        Report report = new Report("TestSuiteName", project);
+        TestSession session = new TestSession("TestSession", project, 
TestSession.SessionType.TEST);
+        Testcase[] tests = new Testcase[] {
+            new Testcase("test1", "TestSuiteName.test1", "TEST", session),
+            new Testcase("test2", "TestSuiteName.test2", "TEST", session)
+        };
+        tests[0].setClassName("TestSuiteName");
+        tests[0].setLocation("TestSuiteName:1");
+        tests[0].setStatus(Status.PASSED);
+        tests[1].setClassName("TestSuiteName");
+        tests[1].setLocation("TestSuiteName:2");
+        tests[1].setStatus(Status.FAILED);
+        Trouble trouble = new Trouble(false);
+        trouble.setStackTrace(new String[] {"TestSuiteName:2"});
+        tests[1].setTrouble(trouble);
+        report.setTests(Arrays.asList(tests));
+        report.setTotalTests(2);
+        report.setPassed(1);
+        report.setFailures(1);
+        progressHandler.displayReport(progressHandler, report);
+        assertEquals("Two messages", 2, msgs.size());
+        assertEquals(fo.toURI().toString(), msgs.get(0).getUri());
+        TestSuiteInfo suite = msgs.get(0).getSuite();
+        assertEquals("TestSuiteName", suite.getSuiteName());
+        assertEquals(TestSuiteInfo.State.Running, suite.getState());
+        assertEquals(fo.toURI().toString(), msgs.get(1).getUri());
+        suite = msgs.get(1).getSuite();
+        assertEquals("TestSuiteName", suite.getSuiteName());
+        assertEquals(TestSuiteInfo.State.Completed, suite.getState());
+        assertEquals(2, suite.getTests().size());
+        TestSuiteInfo.TestCaseInfo testCase = suite.getTests().get(0);
+        assertEquals("TestSuiteName:test1", testCase.getId());
+        assertEquals("test1", testCase.getShortName());
+        assertEquals("TestSuiteName.test1", testCase.getFullName());
+        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.getShortName());
+        assertEquals("TestSuiteName.test2", testCase.getFullName());
+        assertEquals(fo.toURI().toString(), testCase.getFile());
+        assertEquals(TestSuiteInfo.State.Failed, testCase.getState());
+        assertNotNull(testCase.getStackTrace());
+    }
+
+    private static final class MockLanguageClient implements 
NbCodeLanguageClient {
+        private final List<TestProgressParams> messages;
+
+        MockLanguageClient(List<TestProgressParams> messages) {
+            this.messages = messages;
+        }
+
+        @Override
+        public void telemetryEvent(Object object) {
+            fail();
+        }
+
+        @Override
+        public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {
+            fail();
+        }
+
+        @Override
+        public void showMessage(MessageParams messageParams) {
+            fail();
+        }
+
+        @Override
+        public CompletableFuture<MessageActionItem> 
showMessageRequest(ShowMessageRequestParams requestParams) {
+            fail();
+            return null;
+        }
+
+        @Override
+        public void logMessage(MessageParams message) {
+            fail();
+        }
+
+        @Override
+        public void showStatusBarMessage(ShowStatusMessageParams params) {
+            fail();
+        }
+
+        @Override
+        public CompletableFuture<List<QuickPickItem>> 
showQuickPick(ShowQuickPickParams params) {
+            fail();
+            return null;
+        }
+
+        @Override
+        public CompletableFuture<String> showInputBox(ShowInputBoxParams 
params) {
+            fail();
+            return null;
+        }
+
+        @Override
+        public void notifyTestProgress(TestProgressParams params) {
+            messages.add(params);
+        }
+
+        @Override
+        public NbCodeClientCapabilities getNbCodeCapabilities() {
+            fail();
+            return null;
+        }
+    }
+}
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 9b54d39..4b3bd3f 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
@@ -19,7 +19,6 @@
 package org.netbeans.modules.java.lsp.server.protocol;
 
 import com.google.gson.Gson;
-import com.google.gson.JsonElement;
 import com.google.gson.JsonParser;
 import java.io.File;
 import java.io.FileWriter;
@@ -1354,6 +1353,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -2714,6 +2718,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -2908,6 +2917,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -3019,6 +3033,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -3131,6 +3150,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -3230,6 +3254,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -3334,6 +3363,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
@@ -3435,6 +3469,11 @@ public class ServerTest extends NbTestCase {
             }
 
             @Override
+            public void notifyTestProgress(TestProgressParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
             public NbCodeClientCapabilities getNbCodeCapabilities() {
                 throw new UnsupportedOperationException("Not supported yet.");
             }
diff --git a/java/java.lsp.server/vscode/package-lock.json 
b/java/java.lsp.server/vscode/package-lock.json
index 23864b2..c38b96d 100644
--- a/java/java.lsp.server/vscode/package-lock.json
+++ b/java/java.lsp.server/vscode/package-lock.json
@@ -35,7 +35,8 @@
                "@types/ps-node": {
                        "version": "0.1.0",
                        "resolved": 
"https://registry.npmjs.org/@types/ps-node/-/ps-node-0.1.0.tgz";,
-                       "integrity": 
"sha512-HI5l+f38o93x81mbOWZ1IEzj87rGCHfN4A4QyCU1MuViT5Slvlo5F+YVvmBFHfZsEGi0Lo8TghWU2Ew6vBILNA=="
+                       "integrity": 
"sha512-HI5l+f38o93x81mbOWZ1IEzj87rGCHfN4A4QyCU1MuViT5Slvlo5F+YVvmBFHfZsEGi0Lo8TghWU2Ew6vBILNA==",
+                       "dev": true
                },
                "@types/vscode": {
                        "version": "1.49.0",
@@ -911,6 +912,11 @@
                                "is-number": "^7.0.0"
                        }
                },
+               "tslib": {
+                       "version": "1.14.1",
+                       "resolved": 
"https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz";,
+                       "integrity": 
"sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+               },
                "typescript": {
                        "version": "3.9.7",
                        "resolved": 
"https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz";,
@@ -970,6 +976,20 @@
                                "rimraf": "^2.6.3"
                        }
                },
+               "vscode-test-adapter-api": {
+                       "version": "1.9.0",
+                       "resolved": 
"https://registry.npmjs.org/vscode-test-adapter-api/-/vscode-test-adapter-api-1.9.0.tgz";,
+                       "integrity": 
"sha512-lltjehUP0J9H3R/HBctjlqeUCwn2t9Lbhj2Y500ib+j5Y4H3hw+hVTzuSsfw16LtxY37knlU39QIlasa7svzOQ=="
+               },
+               "vscode-test-adapter-util": {
+                       "version": "0.7.1",
+                       "resolved": 
"https://registry.npmjs.org/vscode-test-adapter-util/-/vscode-test-adapter-util-0.7.1.tgz";,
+                       "integrity": 
"sha512-OZZvLDDNhayVVISyTmgUntOhMzl6j9/wVGfNqI2zuR5bQIziTQlDs9W29dFXDTGXZOxazS6uiHkrr86BKDzYUA==",
+                       "requires": {
+                               "tslib": "^1.11.1",
+                               "vscode-test-adapter-api": "^1.8.0"
+                       }
+               },
                "which": {
                        "version": "1.3.1",
                        "resolved": 
"https://registry.npmjs.org/which/-/which-1.3.1.tgz";,
diff --git a/java/java.lsp.server/vscode/package.json 
b/java/java.lsp.server/vscode/package.json
index c357b49..0af9f63 100644
--- a/java/java.lsp.server/vscode/package.json
+++ b/java/java.lsp.server/vscode/package.json
@@ -166,9 +166,9 @@
                "menus": {
                        "editor/context": [
                                {
-                                 "command": "java.goto.super.implementation",
-                                 "when": "nbJavaLSReady && editorLangId == 
java && editorTextFocus",
-                                 "group": "navigation@100"
+                                       "command": 
"java.goto.super.implementation",
+                                       "when": "nbJavaLSReady && editorLangId 
== java && editorTextFocus",
+                                       "group": "navigation@100"
                                }
                        ],
                        "commandPalette": [
@@ -209,6 +209,11 @@
        },
        "dependencies": {
                "vscode-debugadapter": "1.42.1",
-               "vscode-languageclient": "6.1.3"
-       }
+               "vscode-languageclient": "6.1.3",
+               "vscode-test-adapter-api": "^1.9.0",
+               "vscode-test-adapter-util": "^0.7.1"
+       },
+       "extensionDependencies": [
+               "hbenl.vscode-test-explorer"
+       ]
 }
diff --git a/java/java.lsp.server/vscode/src/extension.ts 
b/java/java.lsp.server/vscode/src/extension.ts
index 7135154..86e6e25 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -37,11 +37,15 @@ import * as fs from 'fs';
 import * as path from 'path';
 import { ChildProcess } from 'child_process';
 import * as vscode from 'vscode';
+import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
+import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
 import * as launcher from './nbcode';
-import { StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, 
InputBoxRequest } from './protocol';
+import {NbTestAdapter} from './testAdapter';
+import { StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, 
InputBoxRequest, TestProgressNotification } from './protocol';
 
 const API_VERSION : string = "1.0";
 let client: Promise<LanguageClient>;
+let testAdapterRegistrar: TestAdapterRegistrar<NbTestAdapter>;
 let nbProcess : ChildProcess | null = null;
 let debugPort: number = -1;
 let consoleLog: boolean = !!process.env['ENABLE_CONSOLE_LOG'];
@@ -224,30 +228,52 @@ export function activate(context: ExtensionContext): 
VSNetBeansAPI {
             ]);
         }
     }));
-    const runCodelens = async (uri : any, methodName : string, noDebug : 
boolean) => {
+    const runDebug = async (noDebug : boolean, testRun: boolean, uri : any, 
methodName? : string) => {
         const editor = window.activeTextEditor;
         if (editor) {
             const docUri = editor.document.uri;
             const workspaceFolder = 
vscode.workspace.getWorkspaceFolder(docUri);
             const debugConfig : vscode.DebugConfiguration = {
                 type: "java8+",
-                name: "CodeLens Debug",
+                name: "Java Single Debug",
                 request: "launch",
                 mainClass: uri,
-                singleMethod: methodName,
+                methodName,
+                testRun
             };
             const debugOptions : vscode.DebugSessionOptions = {
                 noDebug: noDebug,
             }
-            await vscode.debug.startDebugging(workspaceFolder, debugConfig, 
debugOptions).then();
+            const ret = await vscode.debug.startDebugging(workspaceFolder, 
debugConfig, debugOptions);
+            return ret ? new Promise((resolve) => {
+                const listener = vscode.debug.onDidTerminateDebugSession(() => 
{
+                    listener.dispose();
+                    resolve(true);
+                });
+            }) : ret;
         }
     };
-    context.subscriptions.push(commands.registerCommand('java.run.codelens', 
async (uri, methodName) => {
-        await runCodelens(uri, methodName, true);
+    context.subscriptions.push(commands.registerCommand('java.run.test', async 
(uri, methodName?) => {
+        await runDebug(true, true, uri, methodName);
+    }));
+    context.subscriptions.push(commands.registerCommand('java.run.single', 
async (uri, methodName?) => {
+        await runDebug(true, false, uri, methodName);
     }));
-    context.subscriptions.push(commands.registerCommand('java.debug.codelens', 
async (uri, methodName) => {
-        await runCodelens(uri, methodName, false);
+    context.subscriptions.push(commands.registerCommand('java.debug.single', 
async (uri, methodName?) => {
+        await runDebug(false, false, uri, methodName);
     }));
+
+       // get the Test Explorer extension and register TestAdapter
+       const testExplorerExtension = 
vscode.extensions.getExtension<TestHub>(testExplorerExtensionId);
+       if (testExplorerExtension) {
+               const testHub = testExplorerExtension.exports;
+        testAdapterRegistrar = new TestAdapterRegistrar(
+                       testHub,
+                       workspaceFolder => new NbTestAdapter(workspaceFolder, 
client)
+               );
+               context.subscriptions.push(testAdapterRegistrar);
+       }
+
     return Object.freeze({
         version : API_VERSION
     });
@@ -451,7 +477,8 @@ function doActivateWithJDK(specifiedJDK: string | null, 
context: ExtensionContex
             progressOnInitialization: true,
             initializationOptions : {
                 'nbcodeCapabilities' : {
-                    'statusBarMessageSupport' : true
+                    'statusBarMessageSupport' : true,
+                    'testResultsSupport' : true
                 }
             },
             errorHandler: {
@@ -487,6 +514,17 @@ function doActivateWithJDK(specifiedJDK: string | null, 
context: ExtensionContex
             c.onRequest(InputBoxRequest.type, async param => {
                 return await window.showInputBox({ prompt: param.prompt, 
value: param.value });
             });
+            c.onNotification(TestProgressNotification.type, param => {
+                if (testAdapterRegistrar) {
+                    const ws = 
workspace.getWorkspaceFolder(vscode.Uri.parse(param.uri));
+                    if (ws) {
+                        const adapter = testAdapterRegistrar.getAdapter(ws);
+                        if (adapter) {
+                            adapter.testProgress(param.suite);
+                        }
+                    }
+                }
+            })
             handleLog(log, 'Language Client: Ready');
             setClient[0](c);
             commands.executeCommand('setContext', 'nbJavaLSReady', true);
diff --git a/java/java.lsp.server/vscode/src/protocol.ts 
b/java/java.lsp.server/vscode/src/protocol.ts
index 6f56c6d..c42bc18 100644
--- a/java/java.lsp.server/vscode/src/protocol.ts
+++ b/java/java.lsp.server/vscode/src/protocol.ts
@@ -69,3 +69,30 @@ export interface ShowInputBoxParams {
 export namespace InputBoxRequest {
     export const type = new RequestType<ShowInputBoxParams, string | 
undefined, void, void>('window/showInputBox');
 }
+
+export interface TestProgressParams {
+    uri: string;
+    suite: TestSuite;
+}
+
+export interface TestSuite {
+    suiteName: string;
+    file?: string;
+    line?: number;
+    state: 'running' | 'completed' | 'errored';
+    tests?: TestCase[];
+}
+
+export interface TestCase {
+    id: string;
+    shortName: string;
+    fullName: string;
+    file?: string;
+    line?: number;
+    state: 'running' | 'passed' | 'failed' | 'skipped' | 'errored';
+    stackTrace?: string[];
+}
+
+export namespace TestProgressNotification {
+    export const type = new NotificationType<TestProgressParams, 
void>('window/notifyTestProgress');
+};
diff --git a/java/java.lsp.server/vscode/src/test/runTest.ts 
b/java/java.lsp.server/vscode/src/test/runTest.ts
index 5ed5f50..747a155 100644
--- a/java/java.lsp.server/vscode/src/test/runTest.ts
+++ b/java/java.lsp.server/vscode/src/test/runTest.ts
@@ -1,8 +1,8 @@
 import * as path from 'path';
 
-import { runTests } from 'vscode-test';
+import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, 
runTests } from 'vscode-test';
 
-import * as os from 'os';
+import * as cp from 'child_process';
 import * as fs from 'fs';
 
 async function main() {
@@ -11,6 +11,14 @@ async function main() {
         // Passed to `--extensionDevelopmentPath`
         const extensionDevelopmentPath = path.resolve(__dirname, '../../');
 
+        const vscodeExecutablePath: string = await 
downloadAndUnzipVSCode('stable');
+        const cliPath: string = 
resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath);
+
+        cp.spawnSync(cliPath, ['--install-extension', 
'hbenl.vscode-test-explorer'], {
+            encoding: 'utf-8',
+            stdio: 'inherit',
+        });
+
         // The path to test runner
         // Passed to --extensionTestsPath
         const extensionTestsPath = path.resolve(__dirname, './suite/index');
@@ -23,7 +31,7 @@ async function main() {
 
         // Download VS Code, unzip it and run the integration test
         await runTests({
-            version: "1.50.0",
+            vscodeExecutablePath,
             extensionDevelopmentPath,
             extensionTestsPath,
             extensionTestsEnv: {
diff --git a/java/java.lsp.server/vscode/src/testAdapter.ts 
b/java/java.lsp.server/vscode/src/testAdapter.ts
new file mode 100644
index 0000000..6beed2c
--- /dev/null
+++ b/java/java.lsp.server/vscode/src/testAdapter.ts
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+'use strict';
+
+import { WorkspaceFolder, Event, EventEmitter, Uri, commands, debug } from 
"vscode";
+import * as path from 'path';
+import { TestAdapter, TestSuiteEvent, TestEvent, TestLoadFinishedEvent, 
TestLoadStartedEvent, TestRunFinishedEvent, TestRunStartedEvent, TestSuiteInfo, 
TestInfo } from "vscode-test-adapter-api";
+import { TestSuite } from "./protocol";
+import { LanguageClient } from "vscode-languageclient";
+import { getVSCodeDownloadUrl } from "vscode-test/out/util";
+
+export class NbTestAdapter implements TestAdapter {
+
+       private disposables: { dispose(): void }[] = [];
+    private children: (TestSuiteInfo | TestInfo)[] = [];
+    private readonly testSuite: TestSuiteInfo;
+
+       private readonly testsEmitter = new EventEmitter<TestLoadStartedEvent | 
TestLoadFinishedEvent>();
+       private readonly statesEmitter = new EventEmitter<TestRunStartedEvent | 
TestRunFinishedEvent | TestSuiteEvent | TestEvent>();
+
+    constructor(
+        public readonly workspaceFolder: WorkspaceFolder,
+        private readonly client: Promise<LanguageClient>
+    ) {
+        this.disposables.push(this.testsEmitter);
+        this.disposables.push(this.statesEmitter);
+        this.testSuite = { type: 'suite', id: '*', label: 'Tests', children: 
this.children };
+    }
+
+       get tests(): Event<TestLoadStartedEvent | TestLoadFinishedEvent> {
+        return this.testsEmitter.event;
+    }
+
+    get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | 
TestSuiteEvent | TestEvent> {
+        return this.statesEmitter.event;
+    }
+
+    async load(): Promise<void> {
+        this.testsEmitter.fire(<TestLoadStartedEvent>{ type: 'started' });
+        let clnt = await this.client;
+        console.log(clnt);
+        this.children.length = 0;
+        const loadedTests: any = await 
commands.executeCommand('java.load.workspace.tests', 
this.workspaceFolder.uri.toString());
+        if (loadedTests) {
+            loadedTests.forEach((suite: TestSuite) => {
+                const children: TestInfo[] = suite.tests ? 
suite.tests.map(test => ({ type: 'test', id: test.id, label: test.shortName, 
tooltip: test.fullName, file: test.file ? Uri.parse(test.file)?.path : 
undefined, line: test.line })) : [];
+                this.children.push({ type: 'suite', id: suite.suiteName, 
label: suite.suiteName, file: suite.file ? Uri.parse(suite.file)?.path : 
undefined, line: suite.line, children });
+            });
+        }
+               this.testsEmitter.fire(<TestLoadFinishedEvent>{ type: 
'finished', suite: this.testSuite });
+    }
+
+    async run(tests: string[]): Promise<void> {
+               this.statesEmitter.fire(<TestRunStartedEvent>{ type: 'started', 
tests });
+               if (tests.length === 1) {
+            if (tests[0] === '*') {
+                await commands.executeCommand('java.run.test', 
this.workspaceFolder.uri.toString());
+                this.statesEmitter.fire(<TestRunFinishedEvent>{ type: 
'finished' });
+            } else {
+                const idx = tests[0].indexOf(':');
+                const suiteName = idx < 0 ? tests[0] : tests[0].slice(0, idx);
+                const current = this.children.find(s => s.id === suiteName);
+                if (current && current.file) {
+                    const methodName = idx < 0 ? undefined : 
tests[0].slice(idx + 1);
+                    if (methodName) {
+                        await commands.executeCommand('java.run.single', 
Uri.file(current.file).toString(), methodName);
+                    } else {
+                        await commands.executeCommand('java.run.single', 
Uri.file(current.file).toString());
+                    }
+                    this.statesEmitter.fire(<TestRunFinishedEvent>{ type: 
'finished' });
+                } else {
+                    this.statesEmitter.fire(<TestLoadFinishedEvent>{ type: 
'finished', errorMessage: `Cannot find suite to run: ${tests[0]}` });
+                }
+            }
+               } else {
+                       this.statesEmitter.fire(<TestLoadFinishedEvent>{ type: 
'finished', errorMessage: 'Failed to run mutliple tests'});
+        }
+    }
+
+    async debug(tests: string[]): Promise<void> {
+               this.statesEmitter.fire(<TestRunStartedEvent>{ type: 'started', 
tests });
+               if (tests.length === 1) {
+            const idx = tests[0].indexOf(':');
+            const suiteName = idx < 0 ? tests[0] : tests[0].slice(0, idx);
+            const current = this.children.find(s => s.id === suiteName);
+            if (current && current.file) {
+                const methodName = idx < 0 ? undefined : tests[0].slice(idx + 
1);
+                if (methodName) {
+                    await commands.executeCommand('java.debug.single', 
Uri.file(current.file).toString(), methodName);
+                } else {
+                    await commands.executeCommand('java.debug.single', 
Uri.file(current.file).toString());
+                }
+                this.statesEmitter.fire(<TestRunFinishedEvent>{ type: 
'finished' });
+            } else {
+                this.statesEmitter.fire(<TestLoadFinishedEvent>{ type: 
'finished', errorMessage: `Cannot find suite to debug: ${tests[0]}` });
+            }
+               } else {
+                       this.statesEmitter.fire(<TestLoadFinishedEvent>{ type: 
'finished', errorMessage: 'Failed to debug mutliple tests'});
+        }
+    }
+
+    cancel(): void {
+        debug.stopDebugging();
+    }
+
+    dispose(): void {
+               this.cancel();
+               for (const disposable of this.disposables) {
+                       disposable.dispose();
+               }
+               this.disposables = [];
+       }
+
+    testProgress(suite: TestSuite): void {
+        this.updateTests(suite);
+        if (suite.state === 'running') {
+            this.statesEmitter.fire(<TestSuiteEvent>{ type: 'suite', suite: 
suite.suiteName, state: suite.state });
+        } else {
+            if (suite.tests) {
+                suite.tests.forEach(test => {
+                    let message;
+                    let decorations;
+                    if (test.stackTrace) {
+                        message = test.stackTrace.join('\n');
+                        const testFile = test.file ? 
Uri.parse(test.file)?.path : undefined;
+                        if (testFile) {
+                            const fileName = path.basename(testFile);
+                            const line = test.stackTrace.map(frame => {
+                                const info = 
frame.match(/^\s*at\s*\S*\((\S*):(\d*)\)$/);
+                                if (info && info.length >= 3 && info[1] === 
fileName) {
+                                    return parseInt(info[2]);
+                                }
+                                return null;
+                            }).find(l => l);
+                            if (line) {
+                                decorations = [{ line: line - 1, message: 
test.stackTrace[0] }];
+                            }
+                        }
+                    }
+                    this.statesEmitter.fire(<TestEvent>{ type: 'test', test: 
test.id, state: test.state, message, decorations });
+                });
+            }
+            this.statesEmitter.fire(<TestSuiteEvent>{ type: 'suite', suite: 
suite.suiteName, state: suite.state });
+        }
+    }
+
+    updateTests(suite: TestSuite): void {
+        let changed = false;
+        const currentSuite = this.children.find(s => s.id === suite.suiteName);
+        if (currentSuite) {
+            const file = suite.file ? Uri.parse(suite.file)?.path : undefined;
+            if (file && currentSuite.file !== file) {
+                currentSuite.file = file;
+                changed = true;
+            }
+            if (suite.line && currentSuite.line !== suite.line) {
+                currentSuite.line = suite.line;
+                changed = true
+            }
+            if (suite.tests) {
+                suite.tests.forEach(test => {
+                    const children: (TestSuiteInfo | TestInfo)[] = [];
+                    let currentTest = (currentSuite as 
TestSuiteInfo).children.find(ti => ti.id === test.id);
+                    if (currentTest) {
+                        children.push(currentTest);
+                        const file = test.file ? Uri.parse(test.file)?.path : 
undefined;
+                        if (file && currentTest.file !== file) {
+                            currentTest.file = file;
+                            changed = true;
+                        }
+                        if (test.line && currentTest.line !== test.line) {
+                            currentTest.line = test.line;
+                            changed = true;
+                        }
+                    } else {
+                        children.push({ type: 'test', id: test.id, label: 
test.shortName, tooltip: test.fullName, file: test.file ? 
Uri.parse(test.file)?.path : undefined, line: test.line });
+                        changed = true;
+                    }
+                    if ((currentSuite as TestSuiteInfo).children.length !== 
children.length) {
+                        changed = true;
+                    }
+                    (currentSuite as TestSuiteInfo).children = children;
+                });
+            }
+        } else {
+            const children: TestInfo[] = suite.tests ? suite.tests.map(test => 
{
+                return { type: 'test', id: test.id, label: test.shortName, 
tooltip: test.fullName, file: test.file ? Uri.parse(test.file)?.path : 
undefined, line: test.line };
+            }) : [];
+            this.children.push({ type: 'suite', id: suite.suiteName, label: 
suite.suiteName, file: suite.file ? Uri.parse(suite.file)?.path : undefined, 
line: suite.line, children });
+            changed = true;
+        }
+        if (changed) {
+            this.testsEmitter.fire(<TestLoadFinishedEvent>{ type: 'finished', 
suite: this.testSuite });
+        }
+    }
+}
diff --git a/java/junit.ui/manifest.mf b/java/junit.ui/manifest.mf
index 691154e..dda88c0 100644
--- a/java/junit.ui/manifest.mf
+++ b/java/junit.ui/manifest.mf
@@ -1,5 +1,5 @@
 Manifest-Version: 1.0
 OpenIDE-Module: org.netbeans.modules.junit.ui
 OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/junit/ui/Bundle.properties
-OpenIDE-Module-Specification-Version: 1.16
+OpenIDE-Module-Specification-Version: 1.17
 
diff --git a/java/junit.ui/nbproject/project.xml 
b/java/junit.ui/nbproject/project.xml
index 621c5b2..f0f3fad 100644
--- a/java/junit.ui/nbproject/project.xml
+++ b/java/junit.ui/nbproject/project.xml
@@ -91,7 +91,7 @@
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>1.0</specification-version>
+                        <specification-version>1.22</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
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 9ec5443..e28cac0 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
@@ -40,6 +40,7 @@ import javax.lang.model.element.TypeElement;
 import javax.lang.model.util.Elements;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
+import javax.swing.text.Position;
 import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.CompilationInfo;
 import org.netbeans.api.java.source.JavaSource.Phase;
@@ -123,7 +124,9 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
                     int end = (int) sp.getEndPosition(tp.getCompilationUnit(), 
tp.getLeaf());
                     Document doc = 
info.getSnapshot().getSource().getDocument(false);
                     try {
-                        result.add(new TestMethod(new SingleMethod(fileObject, 
mn), doc != null ? doc.createPosition(start) : null, doc != null ? 
doc.createPosition(end) : null));
+                        result.add(new 
TestMethod(typeElement.getQualifiedName().toString(), new 
SingleMethod(fileObject, mn),
+                                doc != null ? doc.createPosition(start) : new 
SimplePosition(start),
+                                doc != null ? doc.createPosition(end) : new 
SimplePosition(end)));
                     } catch (BadLocationException ex) {
                         //ignore
                     }
@@ -192,4 +195,18 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
         }
 
     }
+
+    private static class SimplePosition implements Position {
+
+        private final int offset;
+
+        private SimplePosition(int offset) {
+            this.offset = offset;
+        }
+
+        @Override
+        public int getOffset() {
+            return offset;
+        }
+    }
 }
diff --git a/java/testng.ui/manifest.mf b/java/testng.ui/manifest.mf
index 0fca12e..3b9cb78 100644
--- a/java/testng.ui/manifest.mf
+++ b/java/testng.ui/manifest.mf
@@ -1,5 +1,5 @@
 Manifest-Version: 1.0
 OpenIDE-Module: org.netbeans.modules.testng.ui
 OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/testng/ui/Bundle.properties
-OpenIDE-Module-Specification-Version: 1.19
+OpenIDE-Module-Specification-Version: 1.20
 
diff --git a/java/testng.ui/nbproject/project.xml 
b/java/testng.ui/nbproject/project.xml
index 2665fb9..1bbbafb 100644
--- a/java/testng.ui/nbproject/project.xml
+++ b/java/testng.ui/nbproject/project.xml
@@ -170,7 +170,7 @@
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>1.0</specification-version>
+                        <specification-version>1.22</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git 
a/java/testng.ui/src/org/netbeans/modules/testng/ui/actions/TestClassInfoTask.java
 
b/java/testng.ui/src/org/netbeans/modules/testng/ui/actions/TestClassInfoTask.java
index e271969..e252f51 100644
--- 
a/java/testng.ui/src/org/netbeans/modules/testng/ui/actions/TestClassInfoTask.java
+++ 
b/java/testng.ui/src/org/netbeans/modules/testng/ui/actions/TestClassInfoTask.java
@@ -37,6 +37,7 @@ import javax.lang.model.element.TypeElement;
 import javax.lang.model.util.Elements;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
+import javax.swing.text.Position;
 import org.netbeans.api.java.source.CancellableTask;
 import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.CompilationInfo;
@@ -113,13 +114,14 @@ public final class TestClassInfoTask implements 
CancellableTask<CompilationContr
     public static List<TestMethod> computeTestMethods(CompilationInfo info, 
AtomicBoolean cancel, int caretPosIfAny) {
         //TODO: first verify if this is a test class/class in a test source 
group?
         FileObject fileObject = info.getFileObject();
+        ClassTree clazz;
         List<TreePath> methods;
         if (caretPosIfAny == (-1)) {
             Optional<? extends Tree> anyClass = 
info.getCompilationUnit().getTypeDecls().stream().filter(t -> t.getKind() == 
Kind.CLASS).findAny();
             if (!anyClass.isPresent()) {
                 return Collections.emptyList();
             }
-            ClassTree clazz = (ClassTree) anyClass.get();
+            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 {
@@ -128,11 +130,13 @@ public final class TestClassInfoTask implements 
CancellableTask<CompilationContr
                 tp = tp.getParentPath();
             }
             if (tp != null) {
+                clazz = (ClassTree) tp.getParentPath().getLeaf();
                 methods = Collections.singletonList(tp);
             } else {
                 return Collections.emptyList();
             }
         }
+        TypeElement typeElement = (TypeElement) info.getTrees().getElement(new 
TreePath(new TreePath(info.getCompilationUnit()), clazz));
         Elements elements = info.getElements();
         List<TestMethod> result = new ArrayList<>();
         for (TreePath tp : methods) {
@@ -144,22 +148,24 @@ public final class TestClassInfoTask implements 
CancellableTask<CompilationContr
                 List<? extends AnnotationMirror> allAnnotationMirrors = 
elements.getAllAnnotationMirrors(element);
                 for (Iterator<? extends AnnotationMirror> it = 
allAnnotationMirrors.iterator(); it.hasNext();) {
                     AnnotationMirror annotationMirror = it.next();
-                    TypeElement typeElement = (TypeElement) 
annotationMirror.getAnnotationType().asElement();
-                    if 
(typeElement.getQualifiedName().contentEquals(ANNOTATION)) {
+                    TypeElement annTypeElement = (TypeElement) 
annotationMirror.getAnnotationType().asElement();
+                    if 
(annTypeElement.getQualifiedName().contentEquals(ANNOTATION)) {
                         String mn = element.getSimpleName().toString();
                         SourcePositions sp = 
info.getTrees().getSourcePositions();
                         int start = (int) 
sp.getStartPosition(tp.getCompilationUnit(), tp.getLeaf());
                         int end = (int) 
sp.getEndPosition(tp.getCompilationUnit(), tp.getLeaf());
                         Document doc = 
info.getSnapshot().getSource().getDocument(false);
                         try {
-                            result.add(new TestMethod(new 
SingleMethod(fileObject, mn), doc != null ? doc.createPosition(start) : null, 
doc != null ? doc.createPosition(end) : null));
+                            result.add(new 
TestMethod(typeElement.getQualifiedName().toString(), new 
SingleMethod(fileObject, mn),
+                                    doc != null ? doc.createPosition(start) : 
new SimplePosition(start),
+                                    doc != null ? doc.createPosition(end) : 
new SimplePosition(end)));
                         } catch (BadLocationException ex) {
                             //ignore
-                        }
                     }
                 }
             }
         }
+        }
         return result;
     }
 
@@ -187,4 +193,18 @@ public final class TestClassInfoTask implements 
CancellableTask<CompilationContr
         }
 
     }
+
+    private static class SimplePosition implements Position {
+
+        private final int offset;
+
+        private SimplePosition(int offset) {
+            this.offset = offset;
+        }
+
+        @Override
+        public int getOffset() {
+            return offset;
+        }
+    }
 }
diff --git 
a/nbbuild/misc/prepare-bundles/src/main/resources/org/netbeans/prepare/bundles/vscode-test-adapter-util-0.7.1-license
 
b/nbbuild/misc/prepare-bundles/src/main/resources/org/netbeans/prepare/bundles/vscode-test-adapter-util-0.7.1-license
new file mode 100644
index 0000000..a572e90
--- /dev/null
+++ 
b/nbbuild/misc/prepare-bundles/src/main/resources/org/netbeans/prepare/bundles/vscode-test-adapter-util-0.7.1-license
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Holger Benl <hb...@evandor.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org
For additional commands, e-mail: commits-h...@netbeans.apache.org

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

Reply via email to