Skip to content

Commit a4b40f6

Browse files
authored
test: Enforce network hygiene with pytest-subket (#13481)
pytest-subket is my fork of pytest-socket with changes that allow it to function in subprocesses and even across environments (as long as the pytest-subket package is installed, no dependencies needed). To promote better test hygiene, tests should need to opt-in to access the Internet. This is managed via the network marker, but is not enforced. With pytest-subket, tests accessing the Internet w/o the marker will immediately fail. This will encourage us to write tests to avoid hitting the network which will result in a faster, more reliable test suite that won't regress. This also makes the lives of downstream that run our test suite in a sandboxed environment nicer as our network marker should be up to date 24/7 now (barring any tests using remote VCS repositories). * test: Bring network marker up to date with reality All instances of the network marker were removed. Afterwards a full test suite was performed with pytest-subket active. The marker was reapplied to the tests that actually needed it (including tests that hit the network via Git or other VCS software).
1 parent f07a4cd commit a4b40f6

File tree

8 files changed

+36
-14
lines changed

8 files changed

+36
-14
lines changed

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ test = [
5353
"cryptography",
5454
"freezegun",
5555
"installer",
56-
"pytest",
56+
# pytest-subket requires 7.0+
57+
"pytest >= 7.0",
5758
"pytest-cov",
5859
"pytest-rerunfailures",
5960
"pytest-xdist",
61+
"pytest-subket",
6062
"scripttest",
6163
"setuptools",
6264
# macOS (darwin) arm64 always uses virtualenv >= 20.0
@@ -76,6 +78,7 @@ test-common-wheels = [
7678
"wheel",
7779
# As required by pytest-cov.
7880
"coverage >= 4.4",
81+
"pytest-subket >= 0.8.1",
7982
]
8083

8184
[tool.setuptools]
@@ -296,7 +299,7 @@ follow_imports = "skip"
296299
#
297300

298301
[tool.pytest.ini_options]
299-
addopts = "--ignore src/pip/_vendor --ignore tests/tests_cache -r aR --color=yes"
302+
addopts = "--ignore src/pip/_vendor --ignore tests/tests_cache -r aR --color=yes --disable-socket --allow-hosts=localhost"
300303
xfail_strict = true
301304
markers = [
302305
"network: tests that need network",

tests/conftest.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def pytest_collection_modifyitems(config: Config, items: list[pytest.Function])
102102
if item.get_closest_marker("search") and not config.getoption("--run-search"):
103103
item.add_marker(pytest.mark.skip("pip search test skipped"))
104104

105+
# Exempt tests known to use the network from pytest-subket.
106+
if item.get_closest_marker("network") is not None:
107+
item.add_marker(pytest.mark.enable_socket)
108+
105109
if "CI" in os.environ:
106110
# Mark network tests as flaky
107111
if item.get_closest_marker("network") is not None:
@@ -447,6 +451,18 @@ def coverage_install(
447451
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "coverage")
448452

449453

454+
@pytest.fixture(scope="session")
455+
def socket_install(tmpdir_factory: pytest.TempPathFactory, common_wheels: Path) -> Path:
456+
lib_dir = _common_wheel_editable_install(
457+
tmpdir_factory, common_wheels, "pytest_subket"
458+
)
459+
# pytest-subket is only included so it can intercept and block unexpected
460+
# network requests. It should NOT be visible to the pip under test.
461+
dist_info = next(lib_dir.glob("*.dist-info"))
462+
shutil.rmtree(dist_info)
463+
return lib_dir
464+
465+
450466
def install_pth_link(
451467
venv: VirtualEnvironment, project_name: str, lib_dir: Path
452468
) -> None:
@@ -464,6 +480,7 @@ def virtualenv_template(
464480
setuptools_install: Path,
465481
wheel_install: Path,
466482
coverage_install: Path,
483+
socket_install: Path,
467484
) -> VirtualEnvironment:
468485
venv_type: VirtualEnvironmentType
469486
if request.config.getoption("--use-venv"):
@@ -475,9 +492,13 @@ def virtualenv_template(
475492
tmpdir = tmpdir_factory.mktemp("virtualenv")
476493
venv = VirtualEnvironment(tmpdir.joinpath("venv_orig"), venv_type=venv_type)
477494

478-
# Install setuptools, wheel and pip.
495+
# Install setuptools, wheel, pytest-subket, and pip.
479496
install_pth_link(venv, "setuptools", setuptools_install)
480497
install_pth_link(venv, "wheel", wheel_install)
498+
install_pth_link(venv, "pytest_subket", socket_install)
499+
# Also copy pytest-subket's .pth file so it can intercept socket calls.
500+
with open(venv.site / "pytest_socket.pth", "w") as f:
501+
f.write(socket_install.joinpath("pytest_socket.pth").read_text())
481502

482503
pth, dist_info = pip_editable_parts
483504

tests/functional/test_download.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,9 @@ def test_download_file_url_existing_ok_download(
11011101
simple_pkg = shared_data.packages / "simple-1.0.tar.gz"
11021102
url = f"{simple_pkg.as_uri()}#sha256={sha256(downloaded_path_bytes).hexdigest()}"
11031103

1104-
shared_script.pip("download", "-d", str(download_dir), url)
1104+
shared_script.pip(
1105+
"download", "-d", str(download_dir), url, "--disable-pip-version-check"
1106+
)
11051107

11061108
assert downloaded_path_bytes == downloaded_path.read_bytes()
11071109

tests/functional/test_install.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def test_pep518_refuses_invalid_build_system(
155155
assert "pyproject.toml" in result.stderr
156156

157157

158+
@pytest.mark.network
158159
def test_pep518_allows_missing_requires(
159160
script: PipTestEnvironment, data: TestData, common_wheels: Path
160161
) -> None:
@@ -175,6 +176,7 @@ def test_pep518_allows_missing_requires(
175176
assert result.files_created
176177

177178

179+
@pytest.mark.network
178180
@pytest.mark.usefixtures("enable_user_site")
179181
def test_pep518_with_user_pip(
180182
script: PipTestEnvironment, pip_src: Path, data: TestData, common_wheels: Path
@@ -2424,6 +2426,7 @@ def test_install_sends_certs_for_pep518_deps(
24242426
assert environ.get("SSL_CLIENT_CERT", "")
24252427

24262428

2429+
@pytest.mark.network
24272430
def test_install_skip_work_dir_pkg(script: PipTestEnvironment, data: TestData) -> None:
24282431
"""
24292432
Test that install of a package in working directory

tests/functional/test_install_compat.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import os
77
from pathlib import Path
88

9-
import pytest
10-
119
from tests.lib import (
1210
PipTestEnvironment,
1311
TestData,
@@ -16,7 +14,6 @@
1614
)
1715

1816

19-
@pytest.mark.network
2017
def test_debian_egg_name_workaround(
2118
script: PipTestEnvironment,
2219
shared_data: TestData,

tests/functional/test_install_extras.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,11 @@ def test_install_special_extra(
188188
) in result.stderr, str(result)
189189

190190

191+
@pytest.mark.network
191192
def test_install_requirements_no_r_flag(script: PipTestEnvironment) -> None:
192193
"""Beginners sometimes forget the -r and this leads to confusion"""
193194
result = script.pip("install", "requirements.txt", expect_error=True)
194-
assert 'literally named "requirements.txt"' in result.stdout
195+
assert 'literally named "requirements.txt"' in result.stdout, str(result)
195196

196197

197198
@pytest.mark.parametrize(

tests/functional/test_install_report.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def test_skipped_yanked_version(
117117
assert simple_report["metadata"]["version"] == "2.0"
118118

119119

120+
@pytest.mark.network
120121
@pytest.mark.parametrize(
121122
"specifiers",
122123
[
@@ -127,7 +128,6 @@ def test_skipped_yanked_version(
127128
("Paste[openid]==1.7.5.1", "Paste==1.7.5.1"),
128129
],
129130
)
130-
@pytest.mark.network
131131
def test_install_report_index(
132132
script: PipTestEnvironment, tmp_path: Path, specifiers: tuple[str, ...]
133133
) -> None:
@@ -178,7 +178,6 @@ def test_install_report_index_multiple_extras(
178178
assert install_dict["paste"]["requested_extras"] == ["openid", "subprocess"]
179179

180180

181-
@pytest.mark.network
182181
def test_install_report_direct_archive(
183182
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
184183
) -> None:
@@ -297,7 +296,6 @@ def test_install_report_vcs_editable(
297296
assert pip_test_package_report["download_info"]["dir_info"]["editable"] is True
298297

299298

300-
@pytest.mark.network
301299
def test_install_report_local_path_with_extras(
302300
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
303301
) -> None:
@@ -342,7 +340,6 @@ def test_install_report_local_path_with_extras(
342340
assert "requested_extras" not in simple_report
343341

344342

345-
@pytest.mark.network
346343
def test_install_report_editable_local_path_with_extras(
347344
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
348345
) -> None:

tests/unit/test_finder.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ def test_incorrect_case_file_index(data: TestData) -> None:
8181
assert found.link.url.endswith("Dinner-2.0.tar.gz")
8282

8383

84-
@pytest.mark.network
8584
def test_finder_detects_latest_already_satisfied_find_links(data: TestData) -> None:
8685
"""Test PackageFinder detects latest already satisfied using find-links"""
8786
req = install_req_from_line("simple")
@@ -98,7 +97,6 @@ def test_finder_detects_latest_already_satisfied_find_links(data: TestData) -> N
9897
finder.find_requirement(req, True)
9998

10099

101-
@pytest.mark.network
102100
def test_finder_detects_latest_already_satisfied_pypi_links() -> None:
103101
"""Test PackageFinder detects latest already satisfied using pypi links"""
104102
req = install_req_from_line("initools")

0 commit comments

Comments
 (0)