Skip to content

Commit 81f0572

Browse files
authored
Merge pull request #7374 from chrahunt/maint/use-server-fixture-not-internet
Use server fixture for network test
2 parents a25bdab + 1daa8b2 commit 81f0572

File tree

3 files changed

+142
-55
lines changed

3 files changed

+142
-55
lines changed

tests/conftest.py

+66
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@
66
import shutil
77
import subprocess
88
import sys
9+
from contextlib import contextmanager
910

1011
import pytest
1112
import six
13+
from pip._vendor.contextlib2 import ExitStack
1214
from setuptools.wheel import Wheel
1315

1416
from pip._internal.main import main as pip_entry_point
17+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1518
from tests.lib import DATA_DIR, SRC_DIR, TestData
1619
from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key
1720
from tests.lib.path import Path
1821
from tests.lib.scripttest import PipTestEnvironment
22+
from tests.lib.server import make_mock_server, server_running
1923
from tests.lib.venv import VirtualEnvironment
2024

25+
if MYPY_CHECK_RUNNING:
26+
from typing import Dict, Iterable
27+
28+
from tests.lib.server import MockServer as _MockServer, Responder
29+
2130

2231
def pytest_addoption(parser):
2332
parser.addoption(
@@ -404,3 +413,60 @@ def factory():
404413
return str(output_path)
405414

406415
return factory
416+
417+
418+
class MockServer(object):
419+
def __init__(self, server):
420+
# type: (_MockServer) -> None
421+
self._server = server
422+
self._running = False
423+
self.context = ExitStack()
424+
425+
@property
426+
def port(self):
427+
return self._server.port
428+
429+
@property
430+
def host(self):
431+
return self._server.host
432+
433+
def set_responses(self, responses):
434+
# type: (Iterable[Responder]) -> None
435+
assert not self._running, "responses cannot be set on running server"
436+
self._server.mock.side_effect = responses
437+
438+
def start(self):
439+
# type: () -> None
440+
assert not self._running, "running server cannot be started"
441+
self.context.enter_context(server_running(self._server))
442+
self.context.enter_context(self._set_running())
443+
444+
@contextmanager
445+
def _set_running(self):
446+
self._running = True
447+
try:
448+
yield
449+
finally:
450+
self._running = False
451+
452+
def stop(self):
453+
# type: () -> None
454+
assert self._running, "idle server cannot be stopped"
455+
self.context.close()
456+
457+
def get_requests(self):
458+
# type: () -> Dict[str, str]
459+
"""Get environ for each received request.
460+
"""
461+
assert not self._running, "cannot get mock from running server"
462+
return [
463+
call.args[0] for call in self._server.mock.call_args_list
464+
]
465+
466+
467+
@pytest.fixture
468+
def mock_server():
469+
server = make_mock_server()
470+
test_server = MockServer(server)
471+
with test_server.context:
472+
yield test_server

tests/functional/test_install_config.py

+40-50
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import pytest
66

7+
from tests.lib.server import file_response, package_page
8+
79

810
def test_options_from_env_vars(script):
911
"""
@@ -44,26 +46,15 @@ def test_command_line_options_override_env_vars(script, virtualenv):
4446
def test_env_vars_override_config_file(script, virtualenv):
4547
"""
4648
Test that environmental variables override settings in config files.
47-
4849
"""
49-
fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-')
50-
try:
51-
_test_env_vars_override_config_file(script, virtualenv, config_file)
52-
finally:
53-
# `os.close` is a workaround for a bug in subprocess
54-
# https://bugs.python.org/issue3210
55-
os.close(fd)
56-
os.remove(config_file)
57-
58-
59-
def _test_env_vars_override_config_file(script, virtualenv, config_file):
50+
config_file = script.scratch_path / "test-pip.cfg"
6051
# set this to make pip load it
61-
script.environ['PIP_CONFIG_FILE'] = config_file
52+
script.environ['PIP_CONFIG_FILE'] = str(config_file)
6253
# It's important that we test this particular config value ('no-index')
6354
# because there is/was a bug which only shows up in cases in which
6455
# 'config-item' and 'config_item' hash to the same value modulo the size
6556
# of the config dictionary.
66-
(script.scratch_path / config_file).write_text(textwrap.dedent("""\
57+
config_file.write_text(textwrap.dedent("""\
6758
[global]
6859
no-index = 1
6960
"""))
@@ -133,56 +124,55 @@ def test_command_line_appends_correctly(script, data):
133124
), 'stdout: {}'.format(result.stdout)
134125

135126

136-
@pytest.mark.network
137-
def test_config_file_override_stack(script, virtualenv):
127+
def test_config_file_override_stack(
128+
script, virtualenv, mock_server, shared_data
129+
):
138130
"""
139131
Test config files (global, overriding a global config with a
140132
local, overriding all with a command line flag).
141-
142133
"""
143-
fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-')
144-
try:
145-
_test_config_file_override_stack(script, virtualenv, config_file)
146-
finally:
147-
# `os.close` is a workaround for a bug in subprocess
148-
# https://bugs.python.org/issue3210
149-
os.close(fd)
150-
os.remove(config_file)
134+
mock_server.set_responses([
135+
package_page({}),
136+
package_page({}),
137+
package_page({"INITools-0.2.tar.gz": "/files/INITools-0.2.tar.gz"}),
138+
file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")),
139+
])
140+
mock_server.start()
141+
base_address = "http://{}:{}".format(mock_server.host, mock_server.port)
151142

143+
config_file = script.scratch_path / "test-pip.cfg"
152144

153-
def _test_config_file_override_stack(script, virtualenv, config_file):
154145
# set this to make pip load it
155-
script.environ['PIP_CONFIG_FILE'] = config_file
156-
(script.scratch_path / config_file).write_text(textwrap.dedent("""\
146+
script.environ['PIP_CONFIG_FILE'] = str(config_file)
147+
148+
config_file.write_text(textwrap.dedent("""\
157149
[global]
158-
index-url = https://download.zope.org/ppix
159-
"""))
160-
result = script.pip('install', '-vvv', 'INITools', expect_error=True)
161-
assert (
162-
"Getting page https://download.zope.org/ppix/initools" in result.stdout
163-
)
150+
index-url = {}/simple1
151+
""".format(base_address)))
152+
script.pip('install', '-vvv', 'INITools', expect_error=True)
164153
virtualenv.clear()
165-
(script.scratch_path / config_file).write_text(textwrap.dedent("""\
154+
155+
config_file.write_text(textwrap.dedent("""\
166156
[global]
167-
index-url = https://download.zope.org/ppix
157+
index-url = {address}/simple1
168158
[install]
169-
index-url = https://pypi.gocept.com/
170-
"""))
171-
result = script.pip('install', '-vvv', 'INITools', expect_error=True)
172-
assert "Getting page https://pypi.gocept.com/initools" in result.stdout
173-
result = script.pip(
174-
'install', '-vvv', '--index-url', 'https://pypi.org/simple/',
175-
'INITools',
176-
)
177-
assert (
178-
"Getting page http://download.zope.org/ppix/INITools"
179-
not in result.stdout
159+
index-url = {address}/simple2
160+
""".format(address=base_address))
180161
)
181-
assert "Getting page https://pypi.gocept.com/INITools" not in result.stdout
182-
assert (
183-
"Getting page https://pypi.org/simple/initools" in result.stdout
162+
script.pip('install', '-vvv', 'INITools', expect_error=True)
163+
script.pip(
164+
'install', '-vvv', '--index-url', "{}/simple3".format(base_address),
165+
'INITools',
184166
)
185167

168+
mock_server.stop()
169+
requests = mock_server.get_requests()
170+
assert len(requests) == 4
171+
assert requests[0]["PATH_INFO"] == "/simple1/initools/"
172+
assert requests[1]["PATH_INFO"] == "/simple2/initools/"
173+
assert requests[2]["PATH_INFO"] == "/simple3/initools/"
174+
assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz"
175+
186176

187177
def test_options_from_venv_config(script, virtualenv):
188178
"""

tests/lib/server.py

+36-5
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ def blocked_signals():
5151
signal.pthread_sigmask(signal.SIG_SETMASK, old_mask)
5252

5353

54-
class RequestHandler(WSGIRequestHandler):
54+
class _RequestHandler(WSGIRequestHandler):
5555
def make_environ(self):
56-
environ = super(RequestHandler, self).make_environ()
56+
environ = super(_RequestHandler, self).make_environ()
5757

5858
# From pallets/werkzeug#1469, will probably be in release after
5959
# 0.16.0.
@@ -76,7 +76,7 @@ def make_environ(self):
7676
return environ
7777

7878

79-
def mock_wsgi_adapter(mock):
79+
def _mock_wsgi_adapter(mock):
8080
# type: (Callable[[Environ, StartResponse], Responder]) -> Responder
8181
"""Uses a mock to record function arguments and provide
8282
the actual function that should respond.
@@ -91,10 +91,39 @@ def adapter(environ, start_response):
9191

9292
def make_mock_server(**kwargs):
9393
# type: (Any) -> MockServer
94-
kwargs.setdefault("request_handler", RequestHandler)
94+
"""Creates a mock HTTP(S) server listening on a random port on localhost.
95+
96+
The `mock` property of the returned server provides and records all WSGI
97+
interactions, so one approach to testing could be
98+
99+
server = make_mock_server()
100+
server.mock.side_effects = [
101+
page1,
102+
page2,
103+
]
104+
105+
with server_running(server):
106+
# ... use server...
107+
...
108+
109+
assert server.mock.call_count > 0
110+
call_args_list = server.mock.call_args_list
111+
112+
# `environ` is a dictionary defined as per PEP 3333 with the associated
113+
# contents. Additional properties may be added by werkzeug.
114+
environ, _ = call_args_list[0].args
115+
assert environ["PATH_INFO"].startswith("/hello/simple")
116+
117+
Note that the server interactions take place in a different thread, so you
118+
do not want to touch the server.mock within the `server_running` block.
119+
120+
Note also for pip interactions that "localhost" is a "secure origin", so
121+
be careful using this for failure tests of `--trusted-host`.
122+
"""
123+
kwargs.setdefault("request_handler", _RequestHandler)
95124

96125
mock = Mock()
97-
app = mock_wsgi_adapter(mock)
126+
app = _mock_wsgi_adapter(mock)
98127
server = _make_server("localhost", 0, app=app, **kwargs)
99128
server.mock = mock
100129
return server
@@ -103,6 +132,8 @@ def make_mock_server(**kwargs):
103132
@contextmanager
104133
def server_running(server):
105134
# type: (BaseWSGIServer) -> None
135+
"""Context manager for running the provided server in a separate thread.
136+
"""
106137
thread = threading.Thread(target=server.serve_forever)
107138
thread.daemon = True
108139
with blocked_signals():

0 commit comments

Comments
 (0)