Skip to content

Commit 1b56438

Browse files
committed
test_release.py: add test to check wheel and sdist contents
The main purpose of this test is to ensure that whell and sdist are having the contents we expect, and no other contents. Partially, this is to avoid issues described in csernazs#154. It also checks classifiers that they are in-sync with the python version described in the toml.
1 parent 6e7830a commit 1b56438

4 files changed

Lines changed: 221 additions & 3 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ clean: cov-clean doc-clean
3535

3636
.PHONY: test
3737
test: venv
38-
.venv/bin/pytest tests -s -vv
38+
.venv/bin/pytest tests -s -vv --release
3939
.venv/bin/pytest tests -s -vv --ssl
4040

4141
.PHONY: test-pdb
@@ -44,7 +44,7 @@ test-pdb:
4444

4545
.PHONY: cov
4646
cov: cov-clean
47-
.venv/bin/coverage run -m pytest -vv tests
47+
.venv/bin/coverage run -m pytest -vv tests --release
4848
.venv/bin/coverage run -a -m pytest -vv tests --ssl
4949
.venv/bin/coverage xml
5050

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ pre-commit = "^2.20.0"
8181
requires = ["poetry-core>=1.0.0"]
8282
build-backend = "poetry.core.masonry.api"
8383

84-
8584
[tool.pytest.ini_options]
8685
markers = [
8786
"ssl: set up ssl context",
87+
"release: run release tests",
8888
]
8989

9090
[tool.mypy]

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33

44
def pytest_addoption(parser):
55
parser.addoption("--ssl", action="store_true", default=False, help="run ssl tests")
6+
parser.addoption("--release", action="store_true", default=False, help="run release tests")
67

78

89
def pytest_runtest_setup(item):
910
markers = [marker.name for marker in item.iter_markers()]
11+
1012
if not item.config.getoption("--ssl") and "ssl" in markers:
1113
pytest.skip()
1214
if item.config.getoption("--ssl") and "ssl" not in markers:
1315
pytest.skip()
16+
if not item.config.getoption("--release") and "release" in markers:
17+
pytest.skip()

tests/test_release.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# TODO: skip if poetry is not available or add mark to test it explicitly
2+
3+
4+
import email
5+
import re
6+
import shutil
7+
import subprocess
8+
import tarfile
9+
import zipfile
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
from typing import Iterable
13+
from typing import Tuple
14+
15+
import pytest
16+
import toml
17+
18+
pytestmark = pytest.mark.release
19+
20+
NAME = "pytest-httpserver"
21+
NAME_UNDERSCORE = NAME.replace("-", "_")
22+
23+
24+
@pytest.fixture(scope="session")
25+
def pyproject_path() -> Path:
26+
return Path("pyproject.toml")
27+
28+
29+
@pytest.fixture(scope="session")
30+
def pyproject(pyproject_path: Path):
31+
assert pyproject_path.is_file()
32+
with pyproject_path.open() as infile:
33+
pyproject = toml.load(infile)
34+
return pyproject
35+
36+
37+
class Wheel:
38+
def __init__(self, path: Path):
39+
self.path = path
40+
41+
@property
42+
def wheel_out_dir(self) -> Path:
43+
return self.path.parent.joinpath("wheel")
44+
45+
def extract(self):
46+
with zipfile.ZipFile(self.path) as zf:
47+
zf.extractall(self.wheel_out_dir)
48+
49+
def get_meta(self, version: str, name: str = NAME_UNDERSCORE) -> email.message.Message:
50+
metadata_path = self.wheel_out_dir.joinpath(f"{name}-{version}.dist-info", "METADATA")
51+
with metadata_path.open() as metadata_file:
52+
msg = email.message_from_file(metadata_file)
53+
54+
return msg
55+
56+
57+
class Sdist:
58+
def __init__(self, path: Path):
59+
self.path = path
60+
61+
@property
62+
def sdist_out_dir(self) -> Path:
63+
return self.path.parent.joinpath("sdist")
64+
65+
def extract(self):
66+
with tarfile.open(self.path, mode="r:gz") as tf:
67+
tf.extractall(self.sdist_out_dir)
68+
69+
70+
@dataclass
71+
class Build:
72+
wheel: Wheel
73+
sdist: Sdist
74+
75+
def extract(self):
76+
self.wheel.extract()
77+
self.sdist.extract()
78+
79+
80+
@pytest.fixture(scope="session")
81+
def build(request) -> Iterable[Build]:
82+
dist_path = Path("dist").resolve()
83+
if dist_path.is_dir():
84+
shutil.rmtree(dist_path)
85+
86+
try:
87+
subprocess.run(["poetry", "build"], check=True)
88+
assert dist_path.is_dir()
89+
wheels = list(dist_path.glob("*.whl"))
90+
sdists = list(dist_path.glob("*.tar.gz"))
91+
assert len(wheels) == 1
92+
assert len(sdists) == 1
93+
build = Build(wheel=Wheel(wheels[0]), sdist=Sdist(sdists[0]))
94+
build.extract()
95+
yield build
96+
97+
finally:
98+
shutil.rmtree(dist_path)
99+
100+
101+
@pytest.fixture(scope="session")
102+
def version(pyproject) -> str:
103+
return pyproject["tool"]["poetry"]["version"]
104+
105+
106+
def version_to_tuple(version: str) -> Tuple:
107+
return tuple([int(x) for x in version.split(".")])
108+
109+
110+
def test_python_version(build: Build, pyproject):
111+
pyproject_meta = pyproject["tool"]["poetry"]
112+
wheel_meta = build.wheel.get_meta(version=pyproject_meta["version"])
113+
python_dependency = pyproject_meta["dependencies"]["python"]
114+
m = re.match(r">=(\d+\.\d+),<(\d+\.\d+)", python_dependency)
115+
if m:
116+
min_version, max_version = m.groups()
117+
else:
118+
raise ValueError(python_dependency)
119+
120+
min_version_tuple = version_to_tuple(min_version)
121+
max_version_tuple = version_to_tuple(max_version)
122+
123+
for classifier in wheel_meta.get_all("Classifier"):
124+
if classifier.startswith("Programming Language :: Python ::"):
125+
version_tuple = version_to_tuple(classifier.split("::")[-1].strip())
126+
if len(version_tuple) > 1:
127+
assert version_tuple >= min_version_tuple and version_tuple < max_version_tuple
128+
129+
130+
def test_wheel_no_extra_contents(build: Build, version: str):
131+
wheel_dir = build.wheel.wheel_out_dir
132+
wheel_contents = list(wheel_dir.iterdir())
133+
assert len(wheel_contents) == 2
134+
assert wheel_dir.joinpath(NAME_UNDERSCORE).is_dir()
135+
assert wheel_dir.joinpath(f"{NAME_UNDERSCORE}-{version}.dist-info").is_dir()
136+
137+
package_contents = {path.name for path in wheel_dir.joinpath(NAME_UNDERSCORE).iterdir()}
138+
assert package_contents == {
139+
"__init__.py",
140+
"blocking_http_server.py",
141+
"httpserver.py",
142+
"py.typed",
143+
"pytest_plugin.py",
144+
}
145+
146+
147+
def test_sdist_contents(build: Build, version: str):
148+
sdist_base = build.sdist.sdist_out_dir.joinpath(f"pytest_httpserver-{version}")
149+
150+
subdir_contents = {
151+
".": {
152+
"CHANGES.rst",
153+
"CONTRIBUTION.md",
154+
"doc",
155+
"example_pytest.py",
156+
"example.py",
157+
"LICENSE",
158+
"PKG-INFO",
159+
"pyproject.toml",
160+
"pytest_httpserver",
161+
"README.md",
162+
"setup.py",
163+
"tests",
164+
},
165+
"doc": {
166+
"_static",
167+
"api.rst",
168+
"background.rst",
169+
"changes.rst",
170+
"conf.py",
171+
"fixtures.rst",
172+
"guide.rst",
173+
"howto.rst",
174+
"index.rst",
175+
"Makefile",
176+
"tutorial.rst",
177+
"upgrade.rst",
178+
},
179+
"pytest_httpserver": {
180+
"__init__.py",
181+
"blocking_http_server.py",
182+
"httpserver.py",
183+
"py.typed",
184+
"pytest_plugin.py",
185+
},
186+
"tests": {
187+
"assets",
188+
"conftest.py",
189+
"test_blocking_http_server.py",
190+
"test_handler_errors.py",
191+
"test_headers.py",
192+
"test_json_matcher.py",
193+
"test_mixed.py",
194+
"test_oneshot.py",
195+
"test_ordered.py",
196+
"test_permanent.py",
197+
"test_port_changing.py",
198+
"test_querymatcher.py",
199+
"test_querystring.py",
200+
"test_release.py",
201+
"test_ssl.py",
202+
"test_urimatch.py",
203+
"test_wait.py",
204+
"test_with_statement.py",
205+
},
206+
}
207+
208+
for subdir, subdir_content in subdir_contents.items():
209+
contents = {path.name for path in sdist_base.joinpath(subdir).iterdir()}
210+
assert contents == subdir_content
211+
212+
213+
def test_poetry_check():
214+
subprocess.run(["poetry", "check"], check=True)

0 commit comments

Comments
 (0)