Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run & analyze single tests if its possible #884

Merged
merged 5 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
"properties": {
"metals.serverVersion": {
"type": "string",
"default": "0.11.1+114-58f06ff8-SNAPSHOT",
"default": "0.11.1+159-f0addba4-SNAPSHOT",
"markdownDescription": "The version of the Metals server artifact. Requires reloading the window.\n\n**Change only if you know what you're doing**"
},
"metals.serverProperties": {
Expand Down
34 changes: 21 additions & 13 deletions src/test-explorer/analyze-test-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
TestName,
TestRunActions,
TestSuiteResult,
TestSuiteRun,
} from "./types";
import { testItemCollectionToArray } from "./util";

/**
* Analyze results from TestRun and pass inform Test Controller about them.
Expand All @@ -20,23 +22,29 @@ import {
*/
export function analyzeTestRun(
run: TestRunActions,
suites: vscode.TestItem[],
suites: TestSuiteRun[],
testSuitesResults: TestSuiteResult[],
teardown?: () => void
): void {
const results = createResultsMap(testSuitesResults);
for (const testSuite of suites) {
const suiteName = testSuite.id as SuiteName;
for (const { suiteItem, testCases } of suites) {
const suiteName = suiteItem.id as SuiteName;
const result = results.get(suiteName);
if (result != null) {
// if suite run contains test cases (run was started for (single) test case)
if (testCases.length > 0) {
analyzeTestCases(run, result, testCases);
}
// if test suite has children (test cases) do a more fine-grained analyze of results.
if (testSuite.children.size > 0) {
analyzeTestCases(run, result, testSuite);
// run was started for whole suite which has children (e.g. junit one)
else if (suiteItem.children.size > 0) {
const items = testItemCollectionToArray(suiteItem.children);
analyzeTestCases(run, result, items);
} else {
analyzeTestSuite(run, result, testSuite);
analyzeTestSuite(run, result, suiteItem);
}
} else {
run.skipped?.(testSuite);
run.skipped?.(suiteItem);
}
}
teardown?.();
Expand All @@ -61,19 +69,19 @@ function createResultsMap(
function analyzeTestCases(
run: TestRunActions,
result: TestSuiteResult,
testSuite: vscode.TestItem
testCases: vscode.TestItem[]
) {
const testCasesResults = createTestCasesMap(result);
testSuite.children.forEach((child) => {
const testCaseResult = testCasesResults.get(child.id as TestName);
testCases.forEach((test) => {
const testCaseResult = testCasesResults.get(test.id as TestName);

if (testCaseResult?.kind === "passed") {
run.passed?.(child, testCaseResult.duration);
run.passed?.(test, testCaseResult.duration);
} else if (testCaseResult?.kind === "failed") {
const errorMsg = toTestMessage(testCaseResult.error);
run.failed?.(child, errorMsg, testCaseResult.duration);
run.failed?.(test, errorMsg, testCaseResult.duration);
} else {
run.skipped?.(child);
run.skipped?.(test);
}
});
}
Expand Down
76 changes: 57 additions & 19 deletions src/test-explorer/test-run-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import {
import { debugServerFromUri, DebugSession } from "../scalaDebugger";
import { analyzeTestRun } from "./analyze-test-run";
import { testCache } from "./test-cache";
import { DapEvent, TargetUri, TestItemMetadata } from "./types";
import {
DapEvent,
ScalaTestSelection,
ScalaTestSuiteSelection,
TargetUri,
TestItemMetadata,
TestSuiteRun,
} from "./types";

// this id is used to mark DAP sessions created by TestController
// thanks to that debug tracker knows which requests it should track and gather results
Expand Down Expand Up @@ -58,33 +65,62 @@ export async function runHandler(
if (token.isCancellationRequested) {
run.skipped(test);
} else if (data.kind === "testcase" && test.parent) {
const suites = [test.parent];
await runDebugSession(run, noDebug, test.parent, data.targetUri, suites);
continue;
await runDebugSession(run, noDebug, data.targetUri, {
root: test.parent,
suites: [{ suiteItem: test.parent, testCases: [test] }],
});
} else {
const suites = testCache.getTestItemChildren(test);
await runDebugSession(run, noDebug, test, data.targetUri, suites);
const suitesItems = testCache.getTestItemChildren(test);
await runDebugSession(run, noDebug, data.targetUri, {
root: test,
suites: suitesItems.map((suite) => ({
suiteItem: suite,
testCases: [],
})),
});
}
}
run.end();
afterFinished();
}

/**
* User can start 3 different kind of test runs:
* - run a whole build target/package
* - run a whole test suite
* - run a single test case
* This interface describes all these runs and allows to process them in unified way.
* @param root test item which was clicked by the user, can be test case/suite/package
* @param suites suites which are included in this run
*/
export interface RunParams {
root: vscode.TestItem;
suites: TestSuiteRun[];
}

/**
* @param noDebug determines if debug session will be started as a debug or normal run
*/
async function runDebugSession(
run: vscode.TestRun,
noDebug: boolean,
root: vscode.TestItem,
targetUri: TargetUri,
suites: vscode.TestItem[]
runParams: RunParams
): Promise<void> {
const { root, suites } = runParams;
try {
suites.forEach((suite) => {
suite.children.forEach((child) => run.started(child));
run.started(suite);
suite.suiteItem.children.forEach((child) => run.started(child));
run.started(suite.suiteItem);
});
await commands.executeCommand("workbench.action.files.save");
const testsIds = suites.map((c) => c.id);
const session = await createDebugSession(targetUri, testsIds);
const testSuiteSelection: ScalaTestSuiteSelection[] = runParams.suites.map(
(suite) => ({
className: suite.suiteItem.id,
tests: suite.testCases.map((test) => test.label),
})
);
const session = await createDebugSession(targetUri, testSuiteSelection);
if (!session) {
return;
}
Expand Down Expand Up @@ -128,15 +164,17 @@ function createRunQueue(
*/
async function createDebugSession(
targetUri: TargetUri,
testsIds: string[]
classes: ScalaTestSuiteSelection[]
): Promise<DebugSession | undefined> {
const debugSessionParams: ScalaTestSelection = {
target: { uri: targetUri },
classes,
jvmOptions: [],
env: {},
};
return vscode.commands.executeCommand<DebugSession>(
ServerCommands.DebugAdapterStart,
{
targets: [{ uri: targetUri }],
dataKind: "scala-test-suites",
data: testsIds,
}
debugSessionParams
);
}

Expand Down Expand Up @@ -164,7 +202,7 @@ async function startDebugging(session: DebugSession, noDebug: boolean) {
* Retrieves test suite results for current debus session gathered by DAP tracker and passes
* them to the analyzer function. After analysis ends, results are cleaned.
*/
async function analyzeResults(run: vscode.TestRun, suites: vscode.TestItem[]) {
async function analyzeResults(run: vscode.TestRun, suites: TestSuiteRun[]) {
return new Promise<void>((resolve) => {
const disposable = vscode.debug.onDidTerminateDebugSession(
(session: vscode.DebugSession) => {
Expand Down
33 changes: 33 additions & 0 deletions src/test-explorer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,36 @@ export interface TestRunActions {
passed?(test: vscode.TestItem, duration?: number): void;
skipped?(test: vscode.TestItem): void;
}

/**
* Describes suites in test run.
* @param suiteItem test item which corresponds to the test suite
* @param testCases test cases to run from @param suiteItem. If testCases
* is an empty array then it means that all children of test suite should
* be included in test run.
*/
export interface TestSuiteRun {
suiteItem: vscode.TestItem;
testCases: vscode.TestItem[];
}

export interface BuildTargetIdentifier {
uri: TargetUri;
}
export interface ScalaTestSelection {
/** The build target that contains the test classes. */
target: BuildTargetIdentifier;
/** The fully qualified names of the test classes in this target and the tests in this test classes */
classes: ScalaTestSuiteSelection[];
/** The jvm options for the application. */
jvmOptions: string[];
/** The environment variables for the application. */
env?: Record<string, string>;
}

export interface ScalaTestSuiteSelection {
/** The test class to run. */
className: string;
/** The selected tests to run. */
tests: string[];
}
8 changes: 8 additions & 0 deletions src/test-explorer/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export function toVscodeRange(range: Range): vscode.Range {
);
}

export function testItemCollectionToArray(
testCollection: vscode.TestItemCollection
): vscode.TestItem[] {
const tests: vscode.TestItem[] = [];
testCollection.forEach((test) => tests.push(test));
return tests;
}

/**
* Return prefixes of fully qualified name.
* includeSelf = false :
Expand Down
16 changes: 10 additions & 6 deletions src/test/extension/suites/test-explorer/analyze-test-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ suite("Analyze tests results", () => {
test("suite passed", () => {
const [action, results] = getRunActions();
const testItem = testController.createTestItem("TestSuite", "TestSuite");
analyzeTestRun(action, [testItem], passed);
analyzeTestRun(action, [{ suiteItem: testItem, testCases: [] }], passed);
arrayEqual(results.failed, []);
arrayEqual(results.skipped, []);
arrayEqual(results.passed, [{ id: "TestSuite", duration: 100 }]);
Expand All @@ -95,7 +95,7 @@ suite("Analyze tests results", () => {
const [action, results] = getRunActions();
const testItem = testController.createTestItem("TestSuite", "TestSuite");

analyzeTestRun(action, [testItem], failed);
analyzeTestRun(action, [{ suiteItem: testItem, testCases: [] }], failed);
arrayEqual(results.failed, [
{ id: "TestSuite", duration: 100, msg: [{ message: "Error" }] },
]);
Expand All @@ -112,7 +112,7 @@ suite("Analyze tests results", () => {
testController.createTestItem("TestSuite.test3", "test3"),
]);

analyzeTestRun(action, [testItem], failed);
analyzeTestRun(action, [{ suiteItem: testItem, testCases: [] }], failed);
arrayEqual(results.failed, [
{
id: "TestSuite.test1",
Expand All @@ -134,15 +134,19 @@ suite("Analyze tests results", () => {
testController.createTestItem("TestSuite.test3", "test3"),
].forEach((c) => testItem.children.add(c));

analyzeTestRun(action, [testItem], failed);
analyzeTestRun(
action,
[{ suiteItem: testItem, testCases: [child] }],
failed
);
arrayEqual(results.failed, [
{
id: "TestSuite.test1",
duration: 10,
msg: { message: "Error" },
},
]);
arrayEqual(results.passed, [{ id: "TestSuite.test2", duration: 90 }]);
arrayEqual(results.skipped, [{ id: "TestSuite.test3" }]);
arrayEqual(results.passed, []);
arrayEqual(results.skipped, []);
});
});