blob: aa7988ce6898d80339b45485f83a9c43ca3f1585 [file] [log] [blame]
/*
* 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 { commands, debug, tests, workspace, CancellationToken, TestController, TestItem, TestRunProfileKind, TestRunRequest, Uri, TestRun, TestMessage, Location, Position, MarkdownString, TestRunProfile, CancellationTokenSource } from "vscode";
import * as path from 'path';
import { asRange, TestCase, TestSuite } from "./protocol";
import { COMMAND_PREFIX, listeners, TEST_PROGRESS_EVENT } from "./extension";
type SuiteState = 'enqueued' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored';
export class NbTestAdapter {
private readonly testController: TestController;
private disposables: { dispose(): void }[] = [];
private currentRun: TestRun | undefined;
private itemsToRun: Set<TestItem> | undefined;
private started: boolean = false;
private suiteStates: Map<TestItem, SuiteState>;
private parallelRunProfile: TestRunProfile | undefined;
constructor() {
this.testController = tests.createTestController('apacheNetBeansController', 'Apache NetBeans');
const runHandler = (request: TestRunRequest, cancellation: CancellationToken) => this.run(request, cancellation);
this.testController.createRunProfile('Run Tests', TestRunProfileKind.Run, runHandler);
this.testController.createRunProfile('Debug Tests', TestRunProfileKind.Debug, runHandler);
this.disposables.push(this.testController);
this.load();
this.suiteStates = new Map();
}
public registerRunInParallelProfile(projects: string[]) {
if (!this.parallelRunProfile) {
const runHandler = (request: TestRunRequest, cancellation: CancellationToken) => this.run(request, cancellation, true, projects);
this.parallelRunProfile = this.testController.createRunProfile("Run Tests In Parallel", TestRunProfileKind.Run, runHandler, true);
}
this.testController.items.replace([]);
this.load();
}
public testInParallelProfileExist(): boolean {
return this.parallelRunProfile ? true : false;
}
public runTestsWithParallelProfile(projects?: string[]) {
if (this.parallelRunProfile) {
this.run(new TestRunRequest(undefined, undefined, this.parallelRunProfile), new CancellationTokenSource().token, true, projects);
}
}
async load(): Promise<void> {
for (let workspaceFolder of workspace.workspaceFolders || []) {
const loadedTests: any = await commands.executeCommand(COMMAND_PREFIX + '.load.workspace.tests', workspaceFolder.uri.toString());
if (loadedTests) {
loadedTests.forEach((suite: TestSuite) => {
this.updateTests(suite);
});
}
}
}
async run(request: TestRunRequest, cancellation: CancellationToken, testInParallel: boolean = false, projects?: string[]): Promise<void> {
if (!this.currentRun) {
if (!testInParallel) {
commands.executeCommand('workbench.debug.action.focusRepl');
}
cancellation.onCancellationRequested(() => this.cancel());
this.currentRun = this.testController.createTestRun(request);
this.currentRun.token.onCancellationRequested(() => this.cancel());
this.itemsToRun = new Set();
this.started = false;
if (request.include) {
const include = [...new Map(request.include.map(item => !item.uri && item.parent?.uri ? [item.parent.id, item.parent] : [item.id, item])).values()];
for (let item of include) {
if (item.uri) {
this.set(item, 'enqueued');
item.parent?.children.forEach(child => {
if (child.id?.includes(item.id)) {
this.set(child, 'enqueued');
}
})
const idx = item.id.indexOf(':');
const isNestedClass = item.id.includes('$');
const topLevelClassName = item.id.lastIndexOf('.');
let nestedClass: string | undefined;
if (isNestedClass && topLevelClassName > 0) {
nestedClass = idx < 0
? item.id.slice(topLevelClassName + 1)
: item.id.substring(topLevelClassName + 1, idx);
nestedClass = nestedClass.replace('$', '.');
}
if (!cancellation.isCancellationRequested) {
try {
await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.test.single' : COMMAND_PREFIX + '.run.test.single', item.uri.toString(), idx < 0 ? undefined : item.id.slice(idx + 1),
undefined /* configuration */, nestedClass);
} catch(err) {
// test state will be handled in the code below
console.log(err);
}
}
}
}
} else {
this.testController.items.forEach(item => this.set(item, 'enqueued'));
for (let workspaceFolder of workspace.workspaceFolders || []) {
if (!cancellation.isCancellationRequested) {
try {
if (testInParallel) {
await commands.executeCommand(COMMAND_PREFIX + '.run.test', workspaceFolder.uri.toString(), undefined, undefined, undefined, true, projects);
} else {
await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.test': COMMAND_PREFIX + '.run.test', workspaceFolder.uri.toString());
}
} catch(err) {
// test state will be handled in the code below
console.log(err);
}
}
}
}
if (this.started) {
this.itemsToRun.forEach(item => this.set(item, 'skipped'));
}
// TBD - message
else {
this.itemsToRun.forEach(item => this.set(item, 'failed', new TestMessage('Build failure'), false, true));
}
this.itemsToRun = undefined;
this.currentRun.end();
this.currentRun = undefined;
}
}
set(item: TestItem, state: SuiteState, message?: TestMessage | readonly TestMessage[], noPassDown? : boolean, dispatchBuildFailEvent?: boolean): void {
if (this.currentRun) {
switch (state) {
case 'enqueued':
this.dispatchTestEvent(state, item);
this.itemsToRun?.add(item);
this.currentRun.enqueued(item);
break;
case 'skipped':
this.dispatchTestEvent(state, item);
case 'started':
case 'passed':
this.itemsToRun?.delete(item);
this.currentRun[state](item);
break;
case 'failed':
case 'errored':
if (dispatchBuildFailEvent) {
this.dispatchTestEvent(state, item);
}
this.itemsToRun?.delete(item);
this.currentRun[state](item, message || new TestMessage(""));
break;
}
this.suiteStates.set(item, state);
if (!noPassDown) {
item.children.forEach(child => this.set(child, state, message, noPassDown, dispatchBuildFailEvent));
}
}
}
dispatchTestEvent(state: SuiteState, testItem: TestItem): void {
if (testItem.parent && testItem.children.size > 0) {
if (testItem.id.includes(":") && testItem.parent.parent) {
// special case when parameterized test
const testEvent = this.getParametrizedTestEvent(state, testItem);
if (!testEvent) return;
this.dispatchEvent(testEvent);
} else {
this.dispatchEvent({
name: testItem.id,
moduleName: testItem.parent.id,
modulePath: testItem.parent.uri?.path,
state,
});
}
} else if (testItem.children.size === 0) {
const testSuite = testItem.parent;
const parentState = testSuite && this.suiteStates.get(testSuite) ? this.suiteStates.get(testSuite) : state;
if (testSuite) {
let moduleName = testSuite.parent?.id;
let modulePath = testSuite.parent?.uri?.path;
if (testSuite.id.includes(":") && testSuite.parent?.parent) {
// special case when parameterized test
moduleName = testSuite.parent.parent.id;
modulePath = testSuite.parent.parent.uri?.path;
}
const testSuiteEvent: any = {
name: testSuite.id,
moduleName,
modulePath,
state: parentState,
tests: []
}
testSuite?.children.forEach(suite => {
if (suite.id === testItem.id) {
const idx = suite.id.indexOf(':');
if (idx >= 0) {
const name = suite.id.slice(idx + 1);
testSuiteEvent.tests?.push({
id: suite.id,
name,
state
})
}
}
})
this.dispatchEvent(testSuiteEvent);
}
}
}
getParametrizedTestEvent(state: SuiteState, testItem: TestItem): any {
if (!testItem.parent || !testItem.parent.parent) {
return undefined;
}
let name = testItem.parent.id;
const idx = name.indexOf(':');
return {
name,
moduleName: testItem.parent.parent.id,
modulePath: testItem.parent.parent.uri?.path,
state,
tests: [
{
id: name,
name: name.slice(idx + 1),
state
}
]
}
}
dispatchEvent(event: any): void {
const testProgressListeners = listeners.get(TEST_PROGRESS_EVENT);
testProgressListeners?.forEach(listener => {
commands.executeCommand(listener, event);
})
}
cancel(): void {
debug.stopDebugging();
}
dispose(): void {
this.cancel();
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
}
testOutput(output: string): void {
if (this.currentRun && output) {
this.currentRun.appendOutput(output.replace(/\n/g, '\r\n'));
}
}
testProgress(suite: TestSuite): void {
const currentModule = this.testController.items.get(this.getModuleItemId(suite.moduleName));
const currentSuite = currentModule?.children.get(suite.name);
switch (suite.state) {
case 'loaded':
this.updateTests(suite);
break;
case 'started':
this.started = true;
if (currentSuite) {
this.set(currentSuite, 'started');
}
break;
case 'passed':
case "failed":
case 'errored':
case 'skipped':
if (suite.tests) {
this.updateTests(suite, true);
if (currentSuite && currentModule) {
const suiteMessages: TestMessage[] = [];
suite.tests?.forEach(test => {
if (this.currentRun) {
let currentTest = currentSuite.children.get(test.id);
if (!currentTest) {
currentSuite.children.forEach(item => {
if (!currentTest) {
const subName = this.subTestName(item, test);
if (subName) {
currentTest = subName === '()' ? item : item.children.get(test.id);
}
}
});
}
let message: TestMessage | undefined;
if (test.stackTrace) {
message = new TestMessage(this.stacktrace2Message(currentTest?.uri?.toString(), test.stackTrace));
if (currentTest) {
const testUri = currentTest.uri || currentTest.parent?.uri;
if (testUri) {
const fileName = path.basename(testUri.path);
const line = test.stackTrace.map(frame => {
const info = frame.match(/^\s*at[^\(]*\((\S*):(\d*)\)$/);
if (info && info.length >= 3 && info[1] === fileName) {
return parseInt(info[2]);
}
return null;
}).find(l => l);
const pos = line ? new Position(line - 1, 0) : currentTest.range?.start;
if (pos) {
message.location = new Location(testUri, pos);
}
}
} else {
message.location = new Location(currentSuite.uri!, currentSuite.range!.start);
}
}
if (currentTest && test.state !== 'loaded') {
this.set(currentTest, test.state, message, true);
} else if (test.state !== 'passed' && message) {
suiteMessages.push(message);
}
}
});
if (suiteMessages.length > 0) {
this.set(currentSuite, 'errored', suiteMessages, true);
currentSuite.children.forEach(item => this.set(item, 'skipped'));
} else {
this.set(currentSuite, suite.state, undefined, true);
}
this.set(currentModule, this.calculateStateFor(currentModule), undefined, true);
}
}
break;
}
}
calculateStateFor(testItem: TestItem): 'passed' | 'failed' | 'skipped' | 'errored' {
let passed: number = 0;
testItem.children.forEach(item => {
const state = this.suiteStates.get(item);
if (state === 'enqueued' || state === 'failed') return state;
if (state === 'passed') passed++;
})
if (passed > 0) return 'passed';
return 'skipped';
}
updateTests(suite: TestSuite, testExecution?: boolean): void {
const moduleName = this.getModuleItemId(suite.moduleName);
let currentModule = this.testController.items.get(moduleName);
if (!currentModule) {
const parsedName = this.parseModuleName(moduleName);
currentModule = this.testController.createTestItem(moduleName, this.getNameWithIcon(parsedName, 'module'), this.getModulePath(suite));
this.testController.items.add(currentModule);
}
const suiteChildren: TestItem[] = []
let currentSuite = currentModule.children.get(suite.name);
const suiteUri = suite.file ? Uri.parse(suite.file) : undefined;
if (!currentSuite || suiteUri && currentSuite.uri?.toString() !== suiteUri.toString()) {
currentSuite = this.testController.createTestItem(suite.name, this.getNameWithIcon(suite.name, 'class'), suiteUri);
suiteChildren.push(currentSuite);
}
currentModule.children.forEach(suite => suiteChildren.push(suite));
const suiteRange = asRange(suite.range);
if (!testExecution && suiteRange && suiteRange !== currentSuite.range) {
currentSuite.range = suiteRange;
}
const children: TestItem[] = []
const parentTests: Map<TestItem, TestItem[]> = new Map();
suite.tests?.forEach(test => {
let currentTest = currentSuite?.children.get(test.id);
const testUri = test.file ? Uri.parse(test.file) : undefined;
if (currentTest) {
if (testUri && currentTest.uri?.toString() !== testUri?.toString()) {
currentTest = this.testController.createTestItem(test.id, this.getNameWithIcon(test.name, 'method'), testUri);
currentSuite?.children.add(currentTest);
}
const testRange = asRange(test.range);
if (!testExecution && testRange && testRange !== currentTest.range) {
currentTest.range = testRange;
}
children.push(currentTest);
} else {
if (testExecution) {
const parents: Map<TestItem, string> = new Map();
currentSuite?.children.forEach(item => {
const subName = this.subTestName(item, test);
if (subName && '()' !== subName) {
parents.set(item, subName);
}
});
const parent = this.selectParent(parents);
if (parent) {
let arr = parentTests.get(parent.test);
if (!arr) {
parentTests.set(parent.test, arr = []);
children.push(parent.test);
}
arr.push(this.testController.createTestItem(test.id, this.getNameWithIcon(parent.label, 'method')));
}
} else {
currentTest = this.testController.createTestItem(test.id, this.getNameWithIcon(test.name, 'method'), testUri);
currentTest.range = asRange(test.range);
children.push(currentTest);
currentSuite?.children.add(currentTest);
}
}
});
if (testExecution) {
parentTests.forEach((val, key) => {
const item = this.testController.createTestItem(key.id, key.label, key.uri);
item.range = key.range;
item.children.replace(val);
currentSuite?.children.add(item);
});
} else {
currentSuite.children.replace(children);
currentModule.children.replace(suiteChildren);
}
}
getModuleItemId(moduleName?: string): string {
return moduleName?.replace(":", "-") || "";
}
parseModuleName(moduleName: string): string {
if (!this.parallelRunProfile) {
return moduleName.replace(":", "-");
}
const index = moduleName.indexOf(":");
if (index !== -1) {
return moduleName.slice(index + 1);
}
const parts = moduleName.split("-");
return parts[parts.length - 1];
}
getModulePath(suite: TestSuite): Uri {
return Uri.parse(suite.modulePath || "");
}
getNameWithIcon(itemName: string, itemType: 'module' | 'class' | 'method'): string {
switch (itemType) {
case 'module':
return `$(project) ${itemName}`;
case 'class':
return `$(symbol-class) ${itemName}`;
case 'method':
return `$(symbol-method) ${itemName}`;
default:
return itemName;
}
}
getNameWithoutIcon(itemName: string): string {
return itemName.replace(/^\$\([^)]+\)\s*/, "");
}
subTestName(item: TestItem, test: TestCase): string | undefined {
if (test.id.startsWith(item.id)) {
let label = test.name;
const nameWithoutIcon = this.getNameWithoutIcon(item.label);
if (label.startsWith(nameWithoutIcon)) {
label = label.slice(nameWithoutIcon.length).trim();
}
return label;
} else {
const regexp = new RegExp(item.id.replace(/[-[\]{}()*+?.,\\^$|\s]/g, '\\$&').replace(/#\w*/g, '\\S*'));
if (regexp.test(test.id)) {
return test.name;
}
}
return undefined;
}
selectParent(parents: Map<TestItem, string>): {test: TestItem, label: string} | undefined {
let ret: {test: TestItem, label: string} | undefined = undefined;
parents.forEach((label, parentTest) => {
if (ret) {
if (parentTest.id.replace(/#\w*/g, '').length > ret.test.id.replace(/#\w*/g, '').length) {
ret = {test: parentTest, label};
}
} else {
ret = {test: parentTest, label};
}
});
return ret;
}
stacktrace2Message(currentTestUri: string | undefined, stacktrace: string[]): MarkdownString {
const regExp: RegExp = /(\s*at\s+(?:[\w$\\.]+\/)?((?:[\w$]+\.)+[\w\s$<>]+))\(((.*):(\d+))\)/;
const message = new MarkdownString();
message.isTrusted = true;
message.supportHtml = true;
for (const line of stacktrace) {
if (message.value.length) {
message.appendMarkdown('<br/>');
}
const result = regExp.exec(line);
if (result) {
message.appendText(result[1]).appendText('(').appendMarkdown(`[${result[3]}](command:${COMMAND_PREFIX}.open.stacktrace?${encodeURIComponent(JSON.stringify([currentTestUri, result[2], result[4], +result[5]]))})`).appendText(')');
} else {
message.appendText(line);
}
}
return message;
}
}