async getDocumentTestCases()

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,
    }
  }