blob: e1c3ac5e2987bce08aa7a3447e56e98e70e87cf7 [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 } from "vscode";
import * as path from 'path';
import { asRange, TestCase, TestSuite } from "./protocol";
export class NbTestAdapter {
private readonly testController: TestController;
private disposables: { dispose(): void }[] = [];
private currentRun: TestRun | undefined;
private itemsToRun: Set<TestItem> | undefined;
private started: boolean = false;
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();
}
async load(): Promise<void> {
for (let workspaceFolder of workspace.workspaceFolders || []) {
const loadedTests: any = await commands.executeCommand('java.load.workspace.tests', workspaceFolder.uri.toString());
if (loadedTests) {
loadedTests.forEach((suite: TestSuite) => {
this.updateTests(suite);
});
}
}
}
async run(request: TestRunRequest, cancellation: CancellationToken): Promise<void> {
if (!this.currentRun) {
commands.executeCommand('workbench.debug.action.focusRepl');
cancellation.onCancellationRequested(() => this.cancel());
this.currentRun = this.testController.createTestRun(request);
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');
const idx = item.id.indexOf(':');
if (!cancellation.isCancellationRequested) {
await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? 'java.debug.single' : 'java.run.single', item.uri.toString(), idx < 0 ? undefined : item.id.slice(idx + 1));
}
}
}
} else {
this.testController.items.forEach(item => this.set(item, 'enqueued'));
for (let workspaceFolder of workspace.workspaceFolders || []) {
if (!cancellation.isCancellationRequested) {
await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? 'java.debug.test': 'java.run.test', workspaceFolder.uri.toString());
}
}
}
if (this.started) {
this.itemsToRun.forEach(item => this.set(item, 'skipped'));
}
this.itemsToRun = undefined;
this.currentRun.end();
this.currentRun = undefined;
}
}
set(item: TestItem, state: 'enqueued' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored', message?: TestMessage | readonly TestMessage[], noPassDown? : boolean): void {
if (this.currentRun) {
switch (state) {
case 'enqueued':
this.itemsToRun?.add(item);
this.currentRun.enqueued(item);
break;
case 'started':
case 'passed':
case 'skipped':
this.itemsToRun?.delete(item);
this.currentRun[state](item);
break;
case 'failed':
case 'errored':
this.itemsToRun?.delete(item);
this.currentRun[state](item, message || new TestMessage(""));
break;
}
if (!noPassDown) {
item.children.forEach(child => this.set(child, state, message, noPassDown));
}
}
}
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 currentSuite = this.testController.items.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) {
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(test.stackTrace.join('\n'));
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);
}
}
}
break;
}
}
updateTests(suite: TestSuite, testExecution?: boolean): void {
let currentSuite = this.testController.items.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, suite.name, suiteUri);
this.testController.items.add(currentSuite);
}
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 (currentTest.uri?.toString() !== testUri?.toString()) {
currentTest = this.testController.createTestItem(test.id, test.name, 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, parent.label));
}
} else {
currentTest = this.testController.createTestItem(test.id, test.name, 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);
}
}
subTestName(item: TestItem, test: TestCase): string | undefined {
if (test.id.startsWith(item.id)) {
let label = test.name;
if (label.startsWith(item.label)) {
label = label.slice(item.label.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;
}
}