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 a254307cdf65..cf97e0dcecd3 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 @@ -29,6 +29,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -93,14 +95,21 @@ public final CompletableFuture nbLaunch(FileObject toRun, File nativeImage singleMethod = null; } ActionProgress progress = new ActionProgress() { + private final AtomicInteger count = new AtomicInteger(0); + private final AtomicBoolean finalSuccess = new AtomicBoolean(true); @Override protected void started() { + count.incrementAndGet(); } @Override public void finished(boolean success) { - ioContext.stop(); - notifyFinished(context, success); + if (count.decrementAndGet() <= 0) { + ioContext.stop(); + notifyFinished(context, success && finalSuccess.get()); + } else if (!success) { + finalSuccess.set(success); + } } }; if (toRun != null) { 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 0c7e8ce7401a..15433d38320b 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 @@ -52,10 +52,12 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.IntFunction; import java.util.logging.Level; @@ -235,7 +237,7 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli /** * Documents actually opened by the client. */ - private final Map openedDocuments = new HashMap<>(); + private final Map openedDocuments = new ConcurrentHashMap<>(); private final Map diagnosticTasks = new HashMap<>(); private final LspServerState server; private NbCodeLanguageClient client; @@ -1083,6 +1085,8 @@ private static int compareText(CharSequence text1, CharSequence text2) { } //end copied + private ConcurrentHashMap upToDateTests = new ConcurrentHashMap<>(); + @Override public CompletableFuture> codeLens(CodeLensParams params) { // shortcut: if the projects are not yet initialized, return empty: @@ -1099,32 +1103,35 @@ public CompletableFuture> codeLens(CodeLensParams param source.runUserActionTask(cc -> { cc.toPhase(Phase.ELEMENTS_RESOLVED); //look for test methods: - List testMethods = new ArrayList<>(); - for (ComputeTestMethods.Factory methodsFactory : Lookup.getDefault().lookupAll(ComputeTestMethods.Factory.class)) { - testMethods.addAll(methodsFactory.create().computeTestMethods(cc)); - } - if (!testMethods.isEmpty()) { - String testClassName = null; - List 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)); + if (!upToDateTests.getOrDefault(uri, Boolean.FALSE)) { + List testMethods = new ArrayList<>(); + for (ComputeTestMethods.Factory methodsFactory : Lookup.getDefault().lookupAll(ComputeTestMethods.Factory.class)) { + testMethods.addAll(methodsFactory.create().computeTestMethods(cc)); } - 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; + if (!testMethods.isEmpty()) { + String testClassName = null; + List 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))); + upToDateTests.put(uri, Boolean.TRUE); } - client.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(testClassName, uri, line, TestSuiteInfo.State.Loaded, tests))); } //look for main methods: List lens = new ArrayList<>(); @@ -1440,7 +1447,9 @@ public void didOpen(DidOpenTextDocumentParams params) { @Override public void didChange(DidChangeTextDocumentParams params) { - Document doc = openedDocuments.get(params.getTextDocument().getUri()); + String uri = params.getTextDocument().getUri(); + upToDateTests.put(uri, Boolean.FALSE); + Document doc = openedDocuments.get(uri); if (doc != null) { NbDocument.runAtomic((StyledDocument) doc, () -> { for (TextDocumentContentChangeEvent change : params.getContentChanges()) { @@ -1462,10 +1471,12 @@ public void didChange(DidChangeTextDocumentParams params) { @Override public void didClose(DidCloseTextDocumentParams params) { try { + String uri = params.getTextDocument().getUri(); + upToDateTests.remove(uri); // the order here is important ! As the file may cease to exist, it's // important that the doucment is already gone form the client. - openedDocuments.remove(params.getTextDocument().getUri()); - FileObject file = fromURI(params.getTextDocument().getUri(), true); + openedDocuments.remove(uri); + FileObject file = fromURI(uri, true); if (file == null) { return; } 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 28ba394a6512..1723b1f1541d 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 @@ -39,6 +39,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; @@ -179,27 +180,25 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { private CompletableFuture> getTestRootURLs(Project prj) { final Set testRootURLs = new HashSet<>(); - List> futures = new ArrayList<>(); + List contained = null; if (prj != null) { for (SourceGroup sg : ProjectUtils.getSources(prj).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)) { for (URL url : UnitTestForSourceQuery.findUnitTests(sg.getRootFolder())) { testRootURLs.add(url); } } - for (Project containedPrj : ProjectUtils.getContainedProjects(prj, true)) { - boolean testRootFound = false; - for (SourceGroup sg : ProjectUtils.getSources(containedPrj).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)) { + contained = ProjectUtils.getContainedProjects(prj, true).stream().map(p -> p.getProjectDirectory()).collect(Collectors.toList()); + } + return server.asyncOpenSelectedProjects(contained).thenApply(projects -> { + for (Project project : projects) { + for (SourceGroup sg : ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)) { for (URL url : UnitTestForSourceQuery.findUnitTests(sg.getRootFolder())) { testRootURLs.add(url); - testRootFound = true; } } - if (testRootFound) { - futures.add(server.asyncOpenFileOwner(containedPrj.getProjectDirectory())); - } } - } - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).thenApply(value -> testRootURLs); + return testRootURLs; + }); } private void findTestMethods(Set testRootURLs, List testMethods) { diff --git a/java/java.lsp.server/vscode/src/testAdapter.ts b/java/java.lsp.server/vscode/src/testAdapter.ts index 6e9c1a60459f..70896b1bfb39 100644 --- a/java/java.lsp.server/vscode/src/testAdapter.ts +++ b/java/java.lsp.server/vscode/src/testAdapter.ts @@ -20,14 +20,14 @@ 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 { TestAdapter, TestSuiteEvent, TestEvent, TestLoadFinishedEvent, TestLoadStartedEvent, TestRunFinishedEvent, TestRunStartedEvent, TestSuiteInfo, TestInfo, TestDecoration } from "vscode-test-adapter-api"; import { TestSuite } from "./protocol"; import { LanguageClient } from "vscode-languageclient"; export class NbTestAdapter implements TestAdapter { private disposables: { dispose(): void }[] = []; - private children: (TestSuiteInfo | TestInfo)[] = []; + private children: TestSuiteInfo[] = []; private readonly testSuite: TestSuiteInfo; private readonly testsEmitter = new EventEmitter(); @@ -58,7 +58,7 @@ export class NbTestAdapter implements TestAdapter { const loadedTests: any = await commands.executeCommand('java.load.workspace.tests', this.workspaceFolder.uri.toString()); if (loadedTests) { loadedTests.forEach((suite: TestSuite) => { - this.updateTests(suite, false); + this.updateTests(suite); }); } if (this.children.length > 0) { @@ -130,39 +130,57 @@ export class NbTestAdapter implements TestAdapter { } testProgress(suite: TestSuite): void { - this.updateTests(suite, true); - if (suite.state === 'running') { - this.statesEmitter.fire({ type: 'suite', suite: suite.suiteName, state: suite.state }); - } else if (suite.state !== 'loaded') { - 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]); + switch (suite.state) { + case 'loaded': + if (this.updateTests(suite)) { + this.testsEmitter.fire({ type: 'finished', suite: this.testSuite }); + } + break; + case 'running': + this.statesEmitter.fire({ type: 'suite', suite: suite.suiteName, state: suite.state }); + break; + case 'completed': + case 'errored': + let errMessage: string | undefined; + if (suite.tests) { + const currentSuite = this.children.find(s => s.id === suite.suiteName); + if (currentSuite) { + suite.tests.forEach(test => { + let message: string | undefined; + let decorations: TestDecoration[] | undefined; + 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] }]; + } } - return null; - }).find(l => l); - if (line) { - decorations = [{ line: line - 1, message: test.stackTrace[0] }]; } - } + let currentTest = (currentSuite as TestSuiteInfo).children.find(ti => ti.id === test.id); + if (currentTest) { + this.statesEmitter.fire({ type: 'test', test: test.id, state: test.state, message, decorations }); + } else if (test.state !== 'passed' && message && !errMessage) { + suite.state = 'errored'; + errMessage = message; + } + }); } - this.statesEmitter.fire({ type: 'test', test: test.id, state: test.state, message, decorations }); - }); - } - this.statesEmitter.fire({ type: 'suite', suite: suite.suiteName, state: suite.state }); + } + this.statesEmitter.fire({ type: 'suite', suite: suite.suiteName, state: suite.state, message: errMessage }); + break; } } - updateTests(suite: TestSuite, notifyFinish: boolean): void { + updateTests(suite: TestSuite): boolean { let changed = false; const currentSuite = this.children.find(s => s.id === suite.suiteName); if (currentSuite) { @@ -176,11 +194,11 @@ export class NbTestAdapter implements TestAdapter { changed = true } if (suite.tests) { - const children: (TestSuiteInfo | TestInfo)[] = []; + const ids = new Set(); suite.tests.forEach(test => { + ids.add(test.id); 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; @@ -191,14 +209,14 @@ export class NbTestAdapter implements TestAdapter { 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 }); + (currentSuite as TestSuiteInfo).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) { + if ((currentSuite as TestSuiteInfo).children.length !== ids.size) { + (currentSuite as TestSuiteInfo).children = (currentSuite as TestSuiteInfo).children.filter(ti => ids.has(ti.id)); changed = true; } - (currentSuite as TestSuiteInfo).children = children; } } else { const children: TestInfo[] = suite.tests ? suite.tests.map(test => { @@ -207,8 +225,6 @@ export class NbTestAdapter implements TestAdapter { 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 (notifyFinish && changed) { - this.testsEmitter.fire({ type: 'finished', suite: this.testSuite }); - } + return changed; } } diff --git a/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java b/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java index fca1858cce2d..e923bd6a27f3 100644 --- a/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java +++ b/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java @@ -289,6 +289,22 @@ static boolean isFullJavaId(String possibleNewRunningTestClass) { } else { if (relativeFile.exists() && relativeFile.isDirectory()) { outputDir = relativeFile; + } else { + File parentFile = absoluteFile.getParentFile(); + if (parentFile.exists() && parentFile.isDirectory()) { + absoluteFile.mkdir(); + if (absoluteFile.exists() && absoluteFile.isDirectory()) { + outputDir = absoluteFile; + } + } else { + File parentRelativeFile = relativeFile.getParentFile(); + if (parentRelativeFile.exists() && parentRelativeFile.isDirectory()) { + relativeFile.mkdir(); + if (relativeFile.exists() && relativeFile.isDirectory()) { + outputDir = relativeFile; + } + } + } } } if (null != outputDir) {