dotfiles/vscode/.vscode/extensions/donjayamanne.python-environment-manager-1.2.4/pythonFiles/unittestadapter/utils.py
Errol Sancaktar ff17c17e23 vscode
2024-06-14 09:31:58 -06:00

245 lines
7.9 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import argparse
import enum
import inspect
import os
import pathlib
import sys
import unittest
from typing import List, Tuple, Union
script_dir = pathlib.Path(__file__).parent.parent
sys.path.append(os.fspath(script_dir))
sys.path.append(os.fspath(script_dir / "lib" / "python"))
from typing_extensions import TypedDict
# Types
# Inherit from str so it's JSON serializable.
class TestNodeTypeEnum(str, enum.Enum):
class_ = "class"
file = "file"
folder = "folder"
test = "test"
class TestData(TypedDict):
name: str
path: str
type_: TestNodeTypeEnum
id_: str
class TestItem(TestData):
lineno: str
runID: str
class TestNode(TestData):
children: "List[TestNode | TestItem]"
# Helper functions for data retrieval.
def get_test_case(suite):
"""Iterate through a unittest test suite and return all test cases."""
for test in suite:
if isinstance(test, unittest.TestCase):
yield test
else:
for test_case in get_test_case(test):
yield test_case
def get_source_line(obj) -> str:
"""Get the line number of a test case start line."""
try:
sourcelines, lineno = inspect.getsourcelines(obj)
except Exception:
try:
# tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285.
sourcelines, lineno = inspect.getsourcelines(obj.orig_method)
except Exception:
return "*"
# Return the line number of the first line of the test case definition.
for i, v in enumerate(sourcelines):
if v.strip().startswith(("def", "async def")):
return str(lineno + i)
return "*"
# Helper functions for test tree building.
def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode:
"""Build a test node with no children. A test node can be a folder, a file or a class."""
## figure out if we are folder, file, or class
id_gen = path
if type_ == TestNodeTypeEnum.folder or type_ == TestNodeTypeEnum.file:
id_gen = path
else:
# means we have to build test node for class
id_gen = path + "\\" + name
return {"path": path, "name": name, "type_": type_, "children": [], "id_": id_gen}
def get_child_node(
name: str, path: str, type_: TestNodeTypeEnum, root: TestNode
) -> TestNode:
"""Find a child node in a test tree given its name and type. If the node doesn't exist, create it."""
try:
result = next(
node
for node in root["children"]
if node["name"] == name and node["type_"] == type_
)
except StopIteration:
result = build_test_node(path, name, type_)
root["children"].append(result)
return result # type:ignore
def build_test_tree(
suite: unittest.TestSuite, test_directory: str
) -> Tuple[Union[TestNode, None], List[str]]:
"""Build a test tree from a unittest test suite.
This function returns the test tree, and any errors found by unittest.
If no tests were discovered, return `None` and a list of errors (if any).
Test tree structure:
{
"path": <test_directory path>,
"type": "folder",
"name": <folder name>,
"children": [
{ files and folders }
...
{
"path": <file path>,
"name": filename.py,
"type_": "file",
"children": [
{
"path": <class path>,
"name": <class name>,
"type_": "class",
"children": [
{
"path": <test path>,
"name": <test name>,
"type_": "test",
"lineno": <line number>
"id_": <test case id following format in line 196>,
}
],
"id_": <class path path following format after path>
}
],
"id_": <file path>
}
],
"id_": <test_directory path>
}
"""
error = []
directory_path = pathlib.PurePath(test_directory)
root = build_test_node(test_directory, directory_path.name, TestNodeTypeEnum.folder)
for test_case in get_test_case(suite):
test_id = test_case.id()
if test_id.startswith("unittest.loader._FailedTest"):
error.append(str(test_case._exception)) # type: ignore
elif test_id.startswith("unittest.loader.ModuleSkipped"):
components = test_id.split(".")
class_name = f"{components[-1]}.py"
# Find/build class node.
file_path = os.fsdecode(os.path.join(directory_path, class_name))
current_node = get_child_node(
class_name, file_path, TestNodeTypeEnum.file, root
)
else:
# Get the static test path components: filename, class name and function name.
components = test_id.split(".")
*folders, filename, class_name, function_name = components
py_filename = f"{filename}.py"
current_node = root
# Find/build nodes for the intermediate folders in the test path.
for folder in folders:
current_node = get_child_node(
folder,
os.fsdecode(pathlib.PurePath(current_node["path"], folder)),
TestNodeTypeEnum.folder,
current_node,
)
# Find/build file node.
path_components = [test_directory] + folders + [py_filename]
file_path = os.fsdecode(pathlib.PurePath("/".join(path_components)))
current_node = get_child_node(
py_filename, file_path, TestNodeTypeEnum.file, current_node
)
# Find/build class node.
current_node = get_child_node(
class_name, file_path, TestNodeTypeEnum.class_, current_node
)
# Get test line number.
test_method = getattr(test_case, test_case._testMethodName)
lineno = get_source_line(test_method)
# Add test node.
test_node: TestItem = {
"name": function_name,
"path": file_path,
"lineno": lineno,
"type_": TestNodeTypeEnum.test,
"id_": file_path + "\\" + class_name + "\\" + function_name,
"runID": test_id,
} # concatenate class name and function test name
current_node["children"].append(test_node)
if not root["children"]:
root = None
return root, error
def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]:
"""Parse command-line arguments that should be forwarded to unittest to perform discovery.
Valid unittest arguments are: -v, -s, -p, -t and their long-form counterparts,
however we only care about the last three.
The returned tuple contains the following items
- start_directory: The directory where to start discovery, defaults to .
- pattern: The pattern to match test files, defaults to test*.py
- top_level_directory: The top-level directory of the project, defaults to None,
and unittest will use start_directory behind the scenes.
"""
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--start-directory", "-s", default=".")
arg_parser.add_argument("--pattern", "-p", default="test*.py")
arg_parser.add_argument("--top-level-directory", "-t", default=None)
parsed_args, _ = arg_parser.parse_known_args(args)
return (
parsed_args.start_directory,
parsed_args.pattern,
parsed_args.top_level_directory,
)