-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathplugin.py
More file actions
122 lines (103 loc) · 4.04 KB
/
plugin.py
File metadata and controls
122 lines (103 loc) · 4.04 KB
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
120
121
122
#
# Take a look at the example
# https://docs.pytest.org/en/stable/example/nonpython.html
#
import subprocess
from pathlib import Path
import pytest
from .main import extract_from_file, stdout_io
def pytest_addoption(parser):
group = parser.getgroup("general")
group.addoption(
"--codeblocks", action="store_true", help="enable testing of codeblocks"
)
def pytest_collect_file(path, parent):
config = parent.config
path = Path(path)
if config.option.codeblocks and path.suffix == ".md":
return MarkdownFile.from_parent(parent, path=path)
class MarkdownFile(pytest.File):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def collect(self):
for block in extract_from_file(self.path):
if block.syntax not in ["python", "sh", "bash"]:
continue
# https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent
# The name makes sure that tests appear as
# ```
# README.md::line 56
# ```
out = TestBlock.from_parent(parent=self, name=f"line {block.lineno}")
out.obj = block
yield out
class TestBlock(pytest.Item):
def __init__(self, name, parent, obj=None):
super().__init__(name, parent=parent)
self.obj = obj
def runtest(self):
assert self.obj is not None
output = None
if self.obj.skip:
pytest.skip()
if self.obj.skipif is not None:
# needed for sys.version_info in skipif eval():
import sys
if eval(self.obj.skipif):
pytest.skip()
if self.obj.syntax == "python":
if self.obj.expect_exception:
with pytest.raises(Exception):
self.obj.exec({"__MODULE__": "__main__"})
else:
with stdout_io() as s:
try:
# https://stackoverflow.com/a/62851176/353337
self.obj.exec({"__MODULE__": "__main__"})
except Exception as e:
raise RuntimeError(
f"{self.name}, line {self.obj.lineno}:\n```\n"
+ self.obj.code
+ "```\n\n"
+ f"{e}"
)
output = s.getvalue()
else:
assert self.obj.syntax in ["sh", "bash"]
executable = {
"sh": None,
"bash": "/bin/bash",
"zsh": "/bin/zsh",
}[self.obj.syntax]
if self.obj.expect_exception:
with pytest.raises(Exception):
subprocess.run(
self.obj.code, shell=True, check=True, executable=executable
)
else:
# TODO for python 3.7+, stdout=subprocess.PIPE can be replaced
# by capture_output=True
ret = subprocess.run(
self.obj.code,
shell=True,
check=True,
stdout=subprocess.PIPE,
executable=executable,
)
output = ret.stdout.decode()
if output is not None and self.obj.expected_output is not None:
if self.obj.expected_output != output:
raise RuntimeError(
f"{self.name}, line {self.obj.lineno}:\n```\n"
+ f"Expected output\n```\n{self.obj.expected_output}```\n"
+ f"but got\n```\n{output}```"
)
def repr_failure(self, excinfo):
"""Called when self.runtest() raises an exception."""
# if isinstance(excinfo.value, CodeblockException):
return excinfo.value.args[0]
# if excinfo.errisinstance(RuntimeError):
# return excinfo.value.args[0].stdout
# return super().repr_failure(excinfo)
def reportinfo(self):
return (self.path, -1, "code block check")