File: conftest.py

package info (click to toggle)
python-docopt-ng 0.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 396 kB
  • sloc: python: 1,821; makefile: 3
file content (119 lines) | stat: -rw-r--r-- 3,831 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import json
import re
from pathlib import Path
from typing import Generator
from typing import Sequence
from unittest import mock

import pytest

import docopt


def pytest_collect_file(file_path: Path, path, parent):
    if file_path.suffix == ".docopt" and file_path.stem.startswith("test"):
        return DocoptTestFile.from_parent(path=file_path, parent=parent)


def parse_test(raw: str):
    raw = re.compile("#.*$", re.M).sub("", raw).strip()
    if raw.startswith('"""'):
        raw = raw[3:]

    for i, fixture in enumerate(raw.split('r"""')):
        if i == 0:
            if not fixture.strip() == "":
                raise DocoptTestException(
                    f"Unexpected content before first testcase: {fixture}"
                )
            continue

        try:
            doc, _, body = fixture.partition('"""')
            cases = []
            for case in body.split("$")[1:]:
                argv, _, expect = case.strip().partition("\n")
                try:
                    expect = json.loads(expect)
                except json.JSONDecodeError as e:
                    raise DocoptTestException(
                        f"The test case JSON is invalid: {expect!r} - {e}."
                    )
                prog, _, argv = argv.strip().partition(" ")
                cases.append((prog, argv, expect))
            if len(cases) == 0:
                raise DocoptTestException(
                    "No test cases follow the doc. Each example must have at "
                    "least one test case starting with '$'"
                )
        except Exception as e:
            raise DocoptTestException(
                f"Failed to parse test case {i}. {e}\n"
                f'The test\'s definition is:\nr"""{fixture}'
            ) from None
        yield doc, cases


class DocoptTestFile(pytest.File):
    def collect(self):
        raw = self.path.open().read()
        for i, (doc, cases) in enumerate(parse_test(raw), 1):
            name = f"{self.path.stem}({i})"
            for case in cases:
                yield DocoptTestItem.from_parent(
                    name=name, parent=self, doc=doc, case=case
                )


class DocoptTestItem(pytest.Item):
    def __init__(self, name, parent, doc, case):
        super(DocoptTestItem, self).__init__(name, parent)
        self.doc = doc
        self.prog, self.argv, self.expect = case

    def runtest(self):
        try:
            result = docopt.docopt(self.doc, argv=self.argv)
        except docopt.DocoptExit:
            result = "user-error"

        if self.expect != result:
            raise DocoptTestException(self, result)

    def repr_failure(self, excinfo):
        """Called when self.runtest() raises an exception."""
        if isinstance(excinfo.value, DocoptTestException):
            return "\n".join(
                (
                    "usecase execution failed:",
                    self.doc.rstrip(),
                    f"$ {self.prog} {self.argv}",
                    f"result> {json.dumps(excinfo.value.args[1])}",
                    f"expect> {json.dumps(self.expect)}",
                )
            )
        return super().repr_failure(excinfo)

    def reportinfo(self):
        return self.path, 0, f"usecase: {self.name}"


class DocoptTestException(Exception):
    pass


@pytest.fixture(autouse=True)
def override_sys_argv(argv: Sequence[str]) -> Generator[None, None, None]:
    """Patch `sys.argv` with a fixed value during tests.

    A lot of docopt tests call docopt() without specifying argv, which uses
    `sys.argv` by default, so a predictable value for it is necessary.
    """
    with mock.patch("sys.argv", new=argv):
        yield


@pytest.fixture
def argv() -> Sequence[str]:
    """The `sys.argv` value seen inside tests."""
    return ["exampleprogram"]