Skip to content

Commit

Permalink
Test Explorer fixes. (apache#2878)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbalek committed Apr 13, 2021
1 parent 7a9d8b2 commit d100687
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 77 deletions.
Expand Up @@ -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;

Expand Down Expand Up @@ -93,14 +95,21 @@ public final CompletableFuture<Void> 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) {
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -235,7 +237,7 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli
/**
* Documents actually opened by the client.
*/
private final Map<String, Document> openedDocuments = new HashMap<>();
private final Map<String, Document> openedDocuments = new ConcurrentHashMap<>();
private final Map<String, RequestProcessor.Task> diagnosticTasks = new HashMap<>();
private final LspServerState server;
private NbCodeLanguageClient client;
Expand Down Expand Up @@ -1083,6 +1085,8 @@ private static int compareText(CharSequence text1, CharSequence text2) {
}
//end copied

private ConcurrentHashMap<String, Boolean> upToDateTests = new ConcurrentHashMap<>();

@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
// shortcut: if the projects are not yet initialized, return empty:
Expand All @@ -1099,32 +1103,35 @@ public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams param
source.runUserActionTask(cc -> {
cc.toPhase(Phase.ELEMENTS_RESOLVED);
//look for test methods:
List<TestMethod> 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<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));
if (!upToDateTests.getOrDefault(uri, Boolean.FALSE)) {
List<TestMethod> 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<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)));
upToDateTests.put(uri, Boolean.TRUE);
}
client.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(testClassName, uri, line, TestSuiteInfo.State.Loaded, tests)));
}
//look for main methods:
List<CodeLens> lens = new ArrayList<>();
Expand Down Expand Up @@ -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()) {
Expand All @@ -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;
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -179,27 +180,25 @@ public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {

private CompletableFuture<Set<URL>> getTestRootURLs(Project prj) {
final Set<URL> testRootURLs = new HashSet<>();
List<CompletableFuture<?>> futures = new ArrayList<>();
List<FileObject> 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<URL> testRootURLs, List<TestMethodController.TestMethod> testMethods) {
Expand Down
92 changes: 54 additions & 38 deletions java/java.lsp.server/vscode/src/testAdapter.ts
Expand Up @@ -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<TestLoadStartedEvent | TestLoadFinishedEvent>();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -130,39 +130,57 @@ export class NbTestAdapter implements TestAdapter {
}

testProgress(suite: TestSuite): void {
this.updateTests(suite, true);
if (suite.state === 'running') {
this.statesEmitter.fire(<TestSuiteEvent>{ 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(<TestLoadFinishedEvent>{ type: 'finished', suite: this.testSuite });
}
break;
case 'running':
this.statesEmitter.fire(<TestSuiteEvent>{ 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(<TestEvent>{ 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(<TestEvent>{ type: 'test', test: test.id, state: test.state, message, decorations });
});
}
this.statesEmitter.fire(<TestSuiteEvent>{ type: 'suite', suite: suite.suiteName, state: suite.state });
}
this.statesEmitter.fire(<TestSuiteEvent>{ 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) {
Expand All @@ -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;
Expand All @@ -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 => {
Expand All @@ -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(<TestLoadFinishedEvent>{ type: 'finished', suite: this.testSuite });
}
return changed;
}
}

0 comments on commit d100687

Please sign in to comment.