forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Result resolver feature branch (#21457)
fixes #21394
- Loading branch information
1 parent
1323a6a
commit 0f23238
Showing
21 changed files
with
2,091 additions
and
984 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
src/client/testing/testController/common/resultResolver.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; | ||
import * as util from 'util'; | ||
import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; | ||
import { TestProvider } from '../../types'; | ||
import { traceError, traceLog } from '../../../logging'; | ||
import { Testing } from '../../../common/utils/localize'; | ||
import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities'; | ||
import { sendTelemetryEvent } from '../../../telemetry'; | ||
import { EventName } from '../../../telemetry/constants'; | ||
import { splitLines } from '../../../common/stringUtils'; | ||
import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; | ||
|
||
export class PythonResultResolver implements ITestResultResolver { | ||
testController: TestController; | ||
|
||
testProvider: TestProvider; | ||
|
||
public runIdToTestItem: Map<string, TestItem>; | ||
|
||
public runIdToVSid: Map<string, string>; | ||
|
||
public vsIdToRunId: Map<string, string>; | ||
|
||
constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { | ||
this.testController = testController; | ||
this.testProvider = testProvider; | ||
|
||
this.runIdToTestItem = new Map<string, TestItem>(); | ||
this.runIdToVSid = new Map<string, string>(); | ||
this.vsIdToRunId = new Map<string, string>(); | ||
} | ||
|
||
public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise<void> { | ||
const workspacePath = this.workspaceUri.fsPath; | ||
traceLog('Using result resolver for discovery'); | ||
|
||
const rawTestData = payload; | ||
if (!rawTestData) { | ||
// No test data is available | ||
return Promise.resolve(); | ||
} | ||
|
||
// Check if there were any errors in the discovery process. | ||
if (rawTestData.status === 'error') { | ||
const testingErrorConst = | ||
this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; | ||
const { errors } = rawTestData; | ||
traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); | ||
|
||
let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); | ||
const message = util.format( | ||
`${testingErrorConst} ${Testing.seePythonOutput}\r\n`, | ||
errors!.join('\r\n\r\n'), | ||
); | ||
|
||
if (errorNode === undefined) { | ||
const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); | ||
errorNode = createErrorTestItem(this.testController, options); | ||
this.testController.items.add(errorNode); | ||
} | ||
errorNode.error = message; | ||
} else { | ||
// Remove the error node if necessary, | ||
// then parse and insert test data. | ||
this.testController.items.delete(`DiscoveryError:${workspacePath}`); | ||
|
||
if (rawTestData.tests) { | ||
// If the test root for this folder exists: Workspace refresh, update its children. | ||
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. | ||
populateTestTree(this.testController, rawTestData.tests, undefined, this, token); | ||
} else { | ||
// Delete everything from the test controller. | ||
this.testController.items.replace([]); | ||
} | ||
} | ||
|
||
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { | ||
tool: this.testProvider, | ||
failed: false, | ||
}); | ||
return Promise.resolve(); | ||
} | ||
|
||
public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise<void> { | ||
const rawTestExecData = payload; | ||
if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { | ||
// Map which holds the subtest information for each test item. | ||
const subTestStats: Map<string, { passed: number; failed: number }> = new Map(); | ||
|
||
// iterate through payload and update the UI accordingly. | ||
for (const keyTemp of Object.keys(rawTestExecData.result)) { | ||
const testCases: TestItem[] = []; | ||
|
||
// grab leaf level test items | ||
this.testController.items.forEach((i) => { | ||
const tempArr: TestItem[] = getTestCaseNodes(i); | ||
testCases.push(...tempArr); | ||
}); | ||
|
||
if ( | ||
rawTestExecData.result[keyTemp].outcome === 'failure' || | ||
rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' | ||
) { | ||
const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; | ||
const traceback = splitLines(rawTraceback, { | ||
trim: false, | ||
removeEmptyEntries: true, | ||
}).join('\r\n'); | ||
|
||
const text = `${rawTestExecData.result[keyTemp].test} failed: ${ | ||
rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome | ||
}\r\n${traceback}\r\n`; | ||
const message = new TestMessage(text); | ||
|
||
// note that keyTemp is a runId for unittest library... | ||
const grabVSid = this.runIdToVSid.get(keyTemp); | ||
// search through freshly built array of testItem to find the failed test and update UI. | ||
testCases.forEach((indiItem) => { | ||
if (indiItem.id === grabVSid) { | ||
if (indiItem.uri && indiItem.range) { | ||
message.location = new Location(indiItem.uri, indiItem.range); | ||
runInstance.failed(indiItem, message); | ||
runInstance.appendOutput(fixLogLines(text)); | ||
} | ||
} | ||
}); | ||
} else if ( | ||
rawTestExecData.result[keyTemp].outcome === 'success' || | ||
rawTestExecData.result[keyTemp].outcome === 'expected-failure' | ||
) { | ||
const grabTestItem = this.runIdToTestItem.get(keyTemp); | ||
const grabVSid = this.runIdToVSid.get(keyTemp); | ||
if (grabTestItem !== undefined) { | ||
testCases.forEach((indiItem) => { | ||
if (indiItem.id === grabVSid) { | ||
if (indiItem.uri && indiItem.range) { | ||
runInstance.passed(grabTestItem); | ||
runInstance.appendOutput('Passed here'); | ||
} | ||
} | ||
}); | ||
} | ||
} else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { | ||
const grabTestItem = this.runIdToTestItem.get(keyTemp); | ||
const grabVSid = this.runIdToVSid.get(keyTemp); | ||
if (grabTestItem !== undefined) { | ||
testCases.forEach((indiItem) => { | ||
if (indiItem.id === grabVSid) { | ||
if (indiItem.uri && indiItem.range) { | ||
runInstance.skipped(grabTestItem); | ||
runInstance.appendOutput('Skipped here'); | ||
} | ||
} | ||
}); | ||
} | ||
} else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { | ||
// split on " " since the subtest ID has the parent test ID in the first part of the ID. | ||
const parentTestCaseId = keyTemp.split(' ')[0]; | ||
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); | ||
const data = rawTestExecData.result[keyTemp]; | ||
// find the subtest's parent test item | ||
if (parentTestItem) { | ||
const subtestStats = subTestStats.get(parentTestCaseId); | ||
if (subtestStats) { | ||
subtestStats.failed += 1; | ||
} else { | ||
subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); | ||
runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); | ||
// clear since subtest items don't persist between runs | ||
clearAllChildren(parentTestItem); | ||
} | ||
const subtestId = keyTemp; | ||
const subTestItem = this.testController?.createTestItem(subtestId, subtestId); | ||
runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); | ||
// create a new test item for the subtest | ||
if (subTestItem) { | ||
const traceback = data.traceback ?? ''; | ||
const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; | ||
runInstance.appendOutput(fixLogLines(text)); | ||
parentTestItem.children.add(subTestItem); | ||
runInstance.started(subTestItem); | ||
const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); | ||
if (parentTestItem.uri && parentTestItem.range) { | ||
message.location = new Location(parentTestItem.uri, parentTestItem.range); | ||
} | ||
runInstance.failed(subTestItem, message); | ||
} else { | ||
throw new Error('Unable to create new child node for subtest'); | ||
} | ||
} else { | ||
throw new Error('Parent test item not found'); | ||
} | ||
} else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { | ||
// split on " " since the subtest ID has the parent test ID in the first part of the ID. | ||
const parentTestCaseId = keyTemp.split(' ')[0]; | ||
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); | ||
|
||
// find the subtest's parent test item | ||
if (parentTestItem) { | ||
const subtestStats = subTestStats.get(parentTestCaseId); | ||
if (subtestStats) { | ||
subtestStats.passed += 1; | ||
} else { | ||
subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); | ||
runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); | ||
// clear since subtest items don't persist between runs | ||
clearAllChildren(parentTestItem); | ||
} | ||
const subtestId = keyTemp; | ||
const subTestItem = this.testController?.createTestItem(subtestId, subtestId); | ||
// create a new test item for the subtest | ||
if (subTestItem) { | ||
parentTestItem.children.add(subTestItem); | ||
runInstance.started(subTestItem); | ||
runInstance.passed(subTestItem); | ||
runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); | ||
} else { | ||
throw new Error('Unable to create new child node for subtest'); | ||
} | ||
} else { | ||
throw new Error('Parent test item not found'); | ||
} | ||
} | ||
} | ||
} | ||
return Promise.resolve(); | ||
} | ||
} | ||
|
||
// had to switch the order of the original parameter since required param cannot follow optional. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.