使用动态加载的 pytest 装置的可迭代/列表/生成器

问题描述 投票:0回答:1

我正在尝试为 pytest 创建一个动态夹具加载器,它在测试产生值后执行一些工作。一开始,我不需要做任何复杂的事情,所以我只是在准备好后归还夹具:

from pathlib import Path
import shutil

from pytest import CaptureFixture
import pytest

from .tests import testutils

FIXTURES_ROOT = Path(__file__).parent / "fixtures"
INBOX = Path(__file__).parent / "inbox"
CONVERTED = Path(__file__).parent / "converted"


class TestItem:

    converted_dir: Path

    def __init__(self, inbox_dir: Path):
        self.inbox_dir = inbox_dir
        self.converted_dir = CONVERTED / inbox_dir.name

def load_test_fixture(
    name: str,
    *,
    exclusive: bool = False,
    override_name: str | None = None,
    match_filter: str | None = None,
    cleanup_inbox: bool = False,
):
    src = FIXTURES_ROOT / name
    if not src.exists():
        raise FileNotFoundError(
            f"Fixture {name} not found. Does it exist in {FIXTURES_ROOT}?"
        )
    dst = INBOX / (override_name or name)
    dst.mkdir(parents=True, exist_ok=True)

    for f in src.glob("**/*"):
        dst_f = dst / f.relative_to(src)
        if f.is_file() and not dst_f.exists():
            dst_f.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy(f, dst_f)

    # if any files in dst are not in src, delete them
    for f in dst.glob("**/*"):
        src_f = src / f.relative_to(dst)
        if f.is_file() and not src_f.exists():
            f.unlink()

    if exclusive or match_filter is not None:
        testutils.set_match_filter(match_filter or name)

    converted_dir = CONVERTED / (override_name or name)
    shutil.rmtree(converted_dir, ignore_errors=True)

    return TestItem(dst)

这效果很好 - 夹具动态加载,我可以用它创建一堆单行夹具:


@pytest.fixture(scope="function")
def basic_fixture():
    return load_test_fixture("basic_fixture", exclusive=True)

def test_converted_dir_exists(
    basic_fixture: TestItem, capfd: CaptureFixture[str]
):
    assert basic_fixture.converted_dir.exists()

我还创建了一个多夹具加载器,它返回一组夹具:

def load_test_fixtures(
    *names: str,
    exclusive: bool = False,
    override_names: list[str] | None = None,
    match_filter: str | None = None,
):
    if exclusive:
        match_filter = match_filter or rf"^({'|'.join(override_names or names)})"

    return [
        load_test_fixture(name, override_name=override, match_filter=match_filter)
        for (name, override) in zip(names, override_names or names)
    ]

但现在我想让加载器更像一个固定装置,它可以产生项目,然后自行清理。这对于单个加载器来说效果很好:

def load_test_fixture(
    name: str,
    *,
    exclusive: bool = False,
    override_name: str | None = None,
    match_filter: str | None = None,
    cleanup_inbox: bool = False,
):
    # ...
    # instead of returning, yield the item

    yield TestItem(dst)

    if cleanup_inbox:
        shutil.rmtree(dst, ignore_errors=True)

@pytest.fixture(scope="function")
def basic_fixture():
    yield from load_test_fixture("basic_fixture", exclusive=True)

但我无法让它与多重加载器一起工作,因为它返回一个生成器的生成器,或一个生成器列表。我也尝试将它们转换为 @pytest.fixtures ,但似乎无法让列表版本产生其项目并正确等待测试完成后进行清理,并且作为固定装置,我添加了处理作为间接参数或参数传递参数的问题(ew)。

def load_test_fixtures(
    *names: str,
    exclusive: bool = False,
    override_names: list[str] | None = None,
    match_filter: str | None = None,
):
    if exclusive:
        match_filter = match_filter or rf"^({'|'.join(override_names or names)})"

    yield from (
        load_test_fixture(name, match_filter=match_filter, override_name=override)
        for name, override in zip(names, override_names or names)
    )
    # tried all combinations of list, tuple, and yield here, as well as

    # yield (next(load_test_fixture(name, match_filter=match_filter, override_name=override))
    #    for name, override in zip(names, override_names or names)

我什至尝试过这个,这有点有效,但不等待测试结束来运行终结器:

@pytest.fixture(scope="function")
def multi_fixtures():
    fixtures = []
    for f in [
        "basic_fixture",
        "fancy_fixture",
        "tasty_fixture",
        "smart_fixture",
    ]:
        fixtures.extend(
            load_test_fixture(f, match_filter=match_filter, cleanup_inbox=True)
        )
    yield fixtures

(如果我在这里通过

cleanup_inbox=True
,它会在测试运行之前删除文件)。

python python-3.x pytest generator pytest-fixtures
1个回答
0
投票

好吧,这有点恶心,因为它需要从夹具传递

request
参数,但最终这样做是为了解决这个问题。如果有更优雅的方式,我很想知道。

def rm_from_inbox(*names: str):
    for name in names:
        inbox = INBOX / name
        shutil.rmtree(inbox, ignore_errors=True)
        testutils.print(f"Cleaning up {inbox}")

# Updated `load_test_fixture` to also call `rm_from_inbox`:
# ...
# if cleanup_inbox:
#     rm_from_inbox(name)

def load_test_fixtures(
    *names: str,
    exclusive: bool = False,
    override_names: list[str] | None = None,
    match_filter: str | None = None,
    cleanup_inbox: bool = False,
    request: pytest.FixtureRequest | None = None,
):
    if exclusive:
        match_filter = match_filter or rf"^({'|'.join(override_names or names)})"

    fixtures: list[TestItem] = []
    for name, override in zip(names, override_names or names):
        fixtures.extend(
            load_test_fixture(name, match_filter=match_filter, override_name=override)
        )

    if cleanup_inbox:
        if not request:
            raise ValueError(
                "cleanup_inbox requires `request` to be a pytest.FixtureRequest"
            )
        request.addfinalizer(lambda: rm_from_inbox(*names))

    return fixtures

@pytest.fixture(scope="function")
def all_hardy_boys(request: pytest.FixtureRequest):
    return load_test_fixtures(
        "basic_fixture",
        "fancy_fixture",
        "tasty_fixture",
        "smart_fixture",
        exclusive=True,
        request=request,
        cleanup_inbox=True,
    )

© www.soinside.com 2019 - 2024. All rights reserved.