Статически проверить набор тестов Python

У меня есть набор тестов (на основе unittest) для моего проекта Python. Здесь у меня есть тестовые классы, мои методы тестирования и т. д. В (некоторых из моих) тестов я вызываю функцию для инициализации сценариев тестов. Назовем эту функцию generate_scenario(...), и у нее есть куча параметров.

Мне было интересно, могу ли я написать дополнительный код Python, который мог бы найти все случаи вызова generate_scenario(...) и с переданным параметром, чтобы я мог проверить, действительно ли генерируются все «возможные» сценарии.

В идеале мне нужен дополнительный тестовый модуль, чтобы проверить это.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

интересно, можно ли написать код, чтобы найти все случаи вызова методаgener_scenario(...)

Да.

Обратитесь к документации по встроенным модулям «inspect» и «dis». Они очень помогают в работе объекты кода и исходные файлы.

Прилагаю демонстрацию.

Есть как минимум два способа решения вашего варианта использования: простая обработка текста (прославленный grep) и изучение результатов анализа cPython.

from importlib import import_module
from inspect import getsource, isfunction, isgenerator
from pathlib import Path
from types import FunctionType, MethodType, ModuleType
from typing import Callable, Generator, Iterable, NamedTuple
from unittest import TestCase
from unittest.main import TestProgram
import dis
import io
import os
import re
import sys


def find_callable_functions(module: ModuleType | type) -> list[Callable]:
    """Finds callables within a module, including functions and classes."""
    return [
        obj
        for obj in module.__dict__.values()
        if callable(obj) and isinstance(obj, (FunctionType, MethodType, type))
    ]
    # cf inspect.{isfunction, ismethod, isclass}


def find_callable_matches(
    module: ModuleType | type, needle: str, verbose: bool = False
) -> Generator[Callable, None, None]:
    for obj in module.__dict__.values():
        if callable(obj) and isinstance(obj, (FunctionType, MethodType, type)):
            if not isgenerator(obj) and isfunction(obj):
                buf = io.StringIO()
                dis.dis(obj, file=buf)
                names = obj.__code__.co_names
                if needle in buf.getvalue() and needle in names:
                    yield obj
                    if verbose:
                        print(getsource(obj))
                    # print(dis._disassemble_bytes(code, names=names))
                    # lines, start = findsource(obj)
                    # print("".join(lines[start : start + 5]), "\n")
                    # dis.disassemble(obj.__code__)


class Source(NamedTuple):
    """coordinates of a source code location"""

    file: Path
    line: int
    src: list[str]


def find_functions_in(source_file: Path) -> Generator[Source, None, None]:
    decorator = re.compile(r"^\s*@")
    record_delimiter = re.compile(r"^(\s*def |if __name__ == .__main__.)")
    record = Source(Path("/dev/null"), -1, [])  # sentinel
    with open(source_file) as fin:
        for i, line in enumerate(fin):
            if record_delimiter.match(line):
                if record.line > 0:
                    yield record
                record = Source(file=source_file.resolve(), line=i + 1, src=[])
            if not decorator.match(line):
                record.src.append(line)
        if record.line > 0:
            yield record


def find_functions_under(
    paths: Iterable[Path], needle
) -> Generator[Source, None, None]:
    for path in paths:
        if path.is_file() and path.suffix == ".py":
            for record in find_functions_in(path):
                if needle in "".join(record.src):
                    yield record
            # file = f"{record.file.relative_to(os.getcwd())}"
            # m = import_module(file.replace("/", ".").removesuffix(".py"))


class FirstClass:
    def __init__(self, x):
        self.x = x

    def generate_scenario(self, a, b, c):
        self.x += a + b + c

    def run_scenario(self):
        self.generate_scenario(1, 2, 3)
        print(self.x)


class SecondClass:
    def __init__(self, y):
        self.y = y

    def generate_scenario(self, a, b, c):
        self.y += a * b * c

    def run_scenario(self):
        print(self.y)


class UnrelatedClass:
    def __init__(self):
        self.z = None


class TestFindFunctions(TestCase):
    def test_find_callable_functions(self) -> None:
        self.assertEqual(
            [TestProgram],
            find_callable_functions(sys.modules["__main__"]),
        )
        self.assertEqual(
            "<class '_frozen_importlib.FrozenImporter'>",
            str(find_callable_functions(os)[0]),
        )
        self.assertEqual(os, import_module("os"))
        self.assertEqual(
            [
                FirstClass.__init__,
                FirstClass.generate_scenario,
                FirstClass.run_scenario,
            ],
            find_callable_functions(FirstClass),
        )

    def test_find_callable_matches(self) -> None:
        self.assertEqual(
            [FirstClass.run_scenario],
            list(find_callable_matches(FirstClass, "generate_scenario")),
        )

    def test_find_functions(self) -> None:
        source_records = list(find_functions_in(Path(__file__)))
        self.assertEqual(15, len(source_records))

    def test_find_functions_under(self, verbose: bool = False) -> None:
        source_folder = Path(__file__).parent
        glob = source_folder.glob("**/*.py")

        records = list(find_functions_under(glob, "generate_scenario"))
        self.assertEqual(6, len(records))

        if verbose:
            for record in records:
                print(record[:2])
                print("".join(record.src))

Насколько я знаю, проверка предназначена для «живых» объектов, а не для статического анализа. и хотя это может помочь, это очень низкий уровень. Мне было интересно, есть ли какая-нибудь библиотека с более простым в использовании интерфейсом.

Vito De Tullio 27.02.2024 15:32

Другие вопросы по теме