Skip to content

Commit 5d65f08

Browse files
committed
chore: gate pytest-httpserver fork test on python>=3.10
The pytest-httpserver dev dependency requires python>=3.10. Move the fork test (the only consumer) into its own file so the four pre-existing polling-manager tests still run on 3.9, and skip the new file there via importorskip. Add a mypy override so 3.9 type-checks do not error on the missing module.
1 parent ad6cb36 commit 5d65f08

3 files changed

Lines changed: 73 additions & 59 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ pytest-httpserver = {version = "^1.1.0", python = ">=3.10,<4"}
3535
[tool.mypy]
3636
exclude = ["example/*"]
3737

38+
[[tool.mypy.overrides]]
39+
# pytest-httpserver requires python>=3.10, so it is not installed in
40+
# the 3.9 dev environment and mypy cannot find its stubs there.
41+
module = "pytest_httpserver"
42+
ignore_missing_imports = true
43+
3844
[tool.black]
3945
target-version = ["py39"]
4046

tests/test_polling_manager.py

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import json
2-
import multiprocessing
31
import time
4-
from pathlib import Path
52
from unittest import mock
63

7-
import pytest
84
import requests
95
import responses
10-
from pytest_httpserver import HTTPServer
116
from pytest_mock import MockerFixture
127

138
from flagsmith import Flagsmith
@@ -96,57 +91,3 @@ def test_polling_manager_is_resilient_to_request_exceptions(
9691
# Then
9792
assert polling_manager.is_alive()
9893
polling_manager.stop()
99-
100-
101-
@pytest.fixture
102-
def api_url(
103-
httpserver: HTTPServer,
104-
request: pytest.FixtureRequest,
105-
) -> str:
106-
"""A local HTTP server impersonating the Flagsmith environment-document API."""
107-
body = (request.path.parent / "data/environment.json").read_bytes()
108-
httpserver.expect_request("/api/v1/environment-document/").respond_with_data(
109-
body, content_type="application/json"
110-
)
111-
return httpserver.url_for("/api/v1/")
112-
113-
114-
@pytest.mark.skipif(
115-
"fork" not in multiprocessing.get_all_start_methods(),
116-
reason="fork() is unavailable on this platform",
117-
)
118-
def test_polling_manager_keeps_polling_after_fork(api_url: str, tmp_path: Path) -> None:
119-
# Given a flagsmith client polling against a local HTTP server.
120-
flagsmith = Flagsmith(
121-
environment_key="ser.dummy",
122-
api_url=api_url,
123-
enable_local_evaluation=True,
124-
environment_refresh_interval_seconds=0.1,
125-
)
126-
parent_ident = flagsmith.environment_data_polling_manager_thread.ident
127-
128-
def _record_polling_state_in_child(flagsmith: Flagsmith, result_path: Path) -> None:
129-
# Give the child a chance to poll.
130-
time.sleep(0.1)
131-
polling = flagsmith.environment_data_polling_manager_thread
132-
result_path.write_text(
133-
json.dumps({"is_alive": polling.is_alive(), "ident": polling.ident})
134-
)
135-
136-
# Give the parent a chance to poll.
137-
time.sleep(0.1)
138-
result_path = tmp_path / "child_result.json"
139-
140-
# When the process forks (mimicking a gunicorn pre-fork worker).
141-
proc = multiprocessing.get_context("fork").Process(
142-
target=_record_polling_state_in_child, args=(flagsmith, result_path)
143-
)
144-
proc.start()
145-
proc.join()
146-
147-
# Then the polling thread is alive in the child, and it's a fresh
148-
# thread rather than the parent's (dead) one.
149-
result = json.loads(result_path.read_text())
150-
assert result["is_alive"], "polling thread did not survive fork()"
151-
assert result["ident"] != parent_ident
152-
flagsmith.environment_data_polling_manager_thread.stop()

tests/test_polling_manager_fork.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import json
2+
import multiprocessing
3+
import time
4+
import typing
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
# pytest-httpserver requires python>=3.10 — skip the whole module on
10+
# older interpreters where the dep isn't installed.
11+
pytest.importorskip("pytest_httpserver")
12+
13+
from flagsmith import Flagsmith # noqa: E402
14+
15+
if typing.TYPE_CHECKING:
16+
from pytest_httpserver import HTTPServer
17+
18+
19+
@pytest.fixture
20+
def api_url(httpserver: "HTTPServer", request: pytest.FixtureRequest) -> str:
21+
"""A local HTTP server impersonating the Flagsmith environment-document API."""
22+
body = (request.path.parent / "data/environment.json").read_bytes()
23+
httpserver.expect_request("/api/v1/environment-document/").respond_with_data(
24+
body, content_type="application/json"
25+
)
26+
return httpserver.url_for("/api/v1/")
27+
28+
29+
@pytest.mark.skipif(
30+
"fork" not in multiprocessing.get_all_start_methods(),
31+
reason="fork() is unavailable on this platform",
32+
)
33+
def test_polling_manager_keeps_polling_after_fork(api_url: str, tmp_path: Path) -> None:
34+
# Given a flagsmith client polling against a local HTTP server.
35+
flagsmith = Flagsmith(
36+
environment_key="ser.dummy",
37+
api_url=api_url,
38+
enable_local_evaluation=True,
39+
environment_refresh_interval_seconds=0.1,
40+
)
41+
parent_ident = flagsmith.environment_data_polling_manager_thread.ident
42+
43+
def _record_polling_state_in_child(flagsmith: Flagsmith, result_path: Path) -> None:
44+
# Give the child a chance to poll.
45+
time.sleep(0.1)
46+
polling = flagsmith.environment_data_polling_manager_thread
47+
result_path.write_text(
48+
json.dumps({"is_alive": polling.is_alive(), "ident": polling.ident})
49+
)
50+
51+
# Give the parent a chance to poll.
52+
time.sleep(0.1)
53+
result_path = tmp_path / "child_result.json"
54+
55+
# When the process forks (mimicking a gunicorn pre-fork worker).
56+
proc = multiprocessing.get_context("fork").Process(
57+
target=_record_polling_state_in_child, args=(flagsmith, result_path)
58+
)
59+
proc.start()
60+
proc.join()
61+
62+
# Then the polling thread is alive in the child, and it's a fresh
63+
# thread rather than the parent's (dead) one.
64+
result = json.loads(result_path.read_text())
65+
assert result["is_alive"], "polling thread did not survive fork()"
66+
assert result["ident"] != parent_ident
67+
flagsmith.environment_data_polling_manager_thread.stop()

0 commit comments

Comments
 (0)