blob: ec5388f3bcf9c2c67c15ce227dc38f10c13c0531 [file]
/*
* 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, 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[] = [];
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) => {
this.updateTests(suite);
});
}
if (this.children.length > 0) {
this.testsEmitter.fire(<TestLoadFinishedEvent>{ type: 'finished', suite: this.testSuite });
} else {
this.testsEmitter.fire(<TestLoadFinishedEvent>{ type: 'finished' });
}
}
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) {
let methodName;
if (idx >= 0) {
let test = current.children.find(t => t.id === tests[0]);
if (test) {
methodName = tests[0].slice(idx + 1);
} else {
let parents = current.children.filter(ti => tests[0].startsWith(ti.id));
if (parents && parents.length === 1 && parents[0].type === 'suite') {
methodName = parents[0].id.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) {
let methodName;
if (idx >= 0) {
let test = current.children.find(t => t.id === tests[0]);
if (test) {
methodName = tests[0].slice(idx + 1);
} else {
let parents = current.children.filter(ti => tests[0].startsWith(ti.id));
if (parents && parents.length === 1 && parents[0].type === 'suite') {
methodName = parents[0].id.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 {
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) {
if (this.updateTests(suite, true)) {
this.testsEmitter.fire(<TestLoadFinishedEvent>{ type: 'finished', suite: this.testSuite });
}
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] }];
}
}
}
let currentTest = (currentSuite as TestSuiteInfo).children.find(ti => ti.id === test.id);
if (!currentTest) {
let parents = (currentSuite as TestSuiteInfo).children.filter(ti => test.id.startsWith(ti.id));
if (parents && parents.length === 1 && parents[0].type === 'suite') {
currentTest = parents[0].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(<TestSuiteEvent>{ type: 'suite', suite: suite.suiteName, state: suite.state, message: errMessage });
break;
}
}
updateTests(suite: TestSuite, preserveMissingTests?: boolean): boolean {
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) {
const ids: Set<string> = new Set();
const parentSuites: Map<TestSuiteInfo, string[]> = new Map();
suite.tests.forEach(test => {
ids.add(test.id);
let currentTest = (currentSuite as TestSuiteInfo).children.find(ti => ti.id === test.id);
if (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 {
let parents = (currentSuite as TestSuiteInfo).children.filter(ti => test.id.startsWith(ti.id));
if (parents && parents.length === 1) {
let childSuite: TestSuiteInfo = parents[0].type === 'suite' ? parents[0] : { type: 'suite', id: parents[0].id, label: parents[0].label, file: parents[0].file, line: parents[0].line, children: [] };
if (!parentSuites.has(childSuite)) {
parentSuites.set(childSuite, childSuite.children.map(ti => ti.id));
}
if (parents[0].type === 'test') {
(currentSuite as TestSuiteInfo).children[(currentSuite as TestSuiteInfo).children.indexOf(parents[0])] = childSuite;
changed = true;
}
currentTest = childSuite.children.find(ti => ti.id === test.id);
if (currentTest) {
let arr = parentSuites.get(childSuite);
let idx = arr ? arr.indexOf(currentTest.id) : -1;
if (idx >= 0) {
arr?.splice(idx, 1);
}
} else {
let label = test.shortName;
if (label.startsWith(childSuite.label)) {
label = label.slice(childSuite.label.length).trim();
}
childSuite.children.push({ type: 'test', id: test.id, label, tooltip: test.fullName, file: test.file ? Uri.parse(test.file)?.path : undefined, line: test.line });
changed = true;
}
} else {
(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;
}
}
});
parentSuites.forEach((val, key) => {
if (val.length > 0) {
key.children = key.children.filter(ti => val.indexOf(ti.id) < 0);
changed = true;
}
});
if (!preserveMissingTests && (currentSuite as TestSuiteInfo).children.length !== ids.size) {
(currentSuite as TestSuiteInfo).children = (currentSuite as TestSuiteInfo).children.filter(ti => ids.has(ti.id));
changed = true;
}
}
} 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;
}
return changed;
}
}