in src/language-tools/python.ts [66:179]
async getDocumentTestCases(
document: vscode.Uri,
workspaceRoot: string
): Promise<TestFileContents> {
if (!TEST_FILE_REGEX.test(path.basename(document.fsPath))) {
// Exclude files that do not match Python convention of *_test.py or test_*.py
return {
isTestFile: false,
testCases: [],
}
}
// Generate a prefix to be used for the lookup key.
// Converts file path, after the repo root, to a Python module path.
// Example: /path/to/repo/src/test/test_example.py -> src.test.test_example
let fileInfo = path.parse(path.relative(workspaceRoot, document.fsPath))
const lookupKeyBase = `${fileInfo.dir.replaceAll('/', '.')}.${
fileInfo.name
}`
// File name to be included in test filter
const testFilterBase = fileInfo.name
const fullDocTestItem: DocumentTestItem = {
name: path.basename(document.fsPath),
range: new vscode.Range(
new vscode.Position(0, 0),
new vscode.Position(0, 0)
),
uri: document,
testFilter: path.basename(document.fsPath),
}
// Document symbols provided by Pylance.
const symbols: vscode.DocumentSymbol[] | undefined =
await vscode.commands.executeCommand(
'vscode.executeDocumentSymbolProvider',
document
)
const result: DocumentTestItem[] = []
const shouldInclude = new Set<DocumentTestItem>()
const evaluateCurrentSymbol = (
symbol: vscode.DocumentSymbol,
parent: DocumentTestItem | undefined = undefined
) => {
let newItem: DocumentTestItem | undefined
if (symbol.kind === vscode.SymbolKind.Class) {
if (symbol.name.startsWith('Test') || symbol.name.endsWith('Test')) {
// Capture class names that begin with 'Test'
newItem = {
name: symbol.name,
range: symbol.range,
uri: document,
testFilter: `${testFilterBase} and ${symbol.name}`,
parent: parent,
lookupKey: `${lookupKeyBase}.${symbol.name}`,
}
} else {
// Per Python test discovery convention, don't evaluate non-test classes.
return
}
} else if (
// Capture function/method names that begin with 'test'
(symbol.kind === vscode.SymbolKind.Method ||
symbol.kind === vscode.SymbolKind.Function) &&
symbol.name.startsWith('test')
) {
const testFilter = parent
? `${parent.testFilter} and ${symbol.name}`
: `${testFilterBase} and ${symbol.name}`
const lookupKey = parent?.lookupKey
? `${parent.lookupKey}.${symbol.name}`
: `${lookupKeyBase}.${symbol.name}`
newItem = {
name: symbol.name,
range: symbol.selectionRange,
uri: document,
testFilter: testFilter,
parent: parent,
lookupKey: lookupKey,
}
if (parent) shouldInclude.add(parent)
shouldInclude.add(newItem)
}
if (newItem) {
result.push(newItem)
}
for (const child of symbol.children) {
// Recurse through nested symbols.
evaluateCurrentSymbol(child, newItem ?? parent)
}
}
// Start at top level and evaluate each symbol in the document.
if (symbols) {
for (const symbol of symbols) {
evaluateCurrentSymbol(symbol)
}
}
const finalTestCases = result.filter(item => {
return shouldInclude.has(item)
})
return {
isTestFile: true,
testCases: finalTestCases,
documentTest: fullDocTestItem,
}
}