Skip to content

gh-131178: Add tests for http.server command-line interface #132540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 79 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
79db2f2
Add tests for http.server command-line interface
ggqlq Apr 14, 2025
a5e5220
Merge branch 'python:main' into main
ggqlq Apr 15, 2025
2b589bf
add news
ggqlq Apr 15, 2025
e11f8fe
add news
ggqlq Apr 15, 2025
4e008fd
lint
ggqlq Apr 15, 2025
ad76ab1
move a new class into test
ggqlq Apr 16, 2025
5e563d3
Update Lib/http/server.py
ggqlq Apr 16, 2025
86b856e
Update Lib/http/server.py
ggqlq Apr 16, 2025
4d5c2b5
Update Lib/http/server.py
ggqlq Apr 16, 2025
574d6be
Update Lib/http/server.py
ggqlq Apr 16, 2025
c1f3358
Update Lib/http/server.py
ggqlq Apr 16, 2025
01d5fb8
Update Lib/http/server.py
ggqlq Apr 16, 2025
540700f
Update Lib/http/server.py
ggqlq Apr 16, 2025
1bdb0ec
Update Lib/test/test_httpservers.py
ggqlq Apr 16, 2025
38aea9e
Update Lib/test/test_httpservers.py
ggqlq Apr 16, 2025
3277327
update
ggqlq Apr 16, 2025
771263d
Update Lib/http/server.py
ggqlq Apr 16, 2025
3679a76
remove news
ggqlq Apr 16, 2025
6c58710
Update Lib/test/test_httpservers.py
ggqlq Apr 17, 2025
3e4a6aa
Update Lib/test/test_httpservers.py
ggqlq Apr 17, 2025
8e93c5d
Update Lib/test/test_httpservers.py
ggqlq Apr 17, 2025
439c36d
add no argument test and redirect stderr
ggqlq Apr 17, 2025
7e8aedc
wrap some lines to fit into 79 characters
ggqlq Apr 17, 2025
8f3e7ad
wrap some lines to fit into 79 characters(2)
ggqlq Apr 17, 2025
85cb099
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
9417864
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
a5a7d8c
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
a35e0d6
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
e2266c0
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
b4f9e72
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
a61b5b1
Update Lib/test/test_httpservers.py
ggqlq Apr 18, 2025
fd70932
update
ggqlq Apr 18, 2025
348e256
update
ggqlq Apr 19, 2025
4c315b0
update(2)
ggqlq Apr 19, 2025
a57b959
add cli test
ggqlq May 5, 2025
b627e02
add cli test(1)
ggqlq May 5, 2025
41065c2
add cli test(2)
ggqlq May 7, 2025
e7b7dff
Merge branch 'main' into main
ggqlq May 9, 2025
cbda832
move runtime tests into a new class
ggqlq May 9, 2025
86847e8
update
ggqlq May 10, 2025
7eb1572
Merge branch 'main' into main
ggqlq May 10, 2025
d504760
update
ggqlq May 10, 2025
811d86d
remove test_cli_flag function
ggqlq May 16, 2025
e5df251
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
1843c80
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
8e8b755
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
2f742d9
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
b5c5ab0
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
05aea06
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
333a761
split stdout and stderr, remove output check after self.assertRaises(…
ggqlq May 16, 2025
4304354
split tls tests
ggqlq May 16, 2025
6c4c134
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
7b3bb1d
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
eed4228
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
7c1713e
Update Lib/test/test_httpservers.py
ggqlq May 16, 2025
a34fa51
Update Lib/test/test_httpservers.py
ggqlq May 17, 2025
8f73c22
make invoke_httpd function return a pair
ggqlq May 16, 2025
2daf3f8
rename vars in wait_for_server function
ggqlq May 17, 2025
1c67654
use call_args = self.args | dict(...)
ggqlq May 17, 2025
9639219
rename 'random.bin' as 'served_filename'
ggqlq May 17, 2025
4156e78
fix indentation in test_missing_tls_cert_flag
ggqlq May 17, 2025
0522a17
add docstring for wait_for_server
ggqlq May 17, 2025
2c9612b
Merge branch 'main' into main
ggqlq May 17, 2025
1b5b3f8
capture the output outside invoke_httpd
ggqlq May 17, 2025
5637928
update terminate processes in test_http_client and test_https_client
ggqlq May 17, 2025
4a6b779
Merge branch 'main' into main
ggqlq May 17, 2025
6beb7f1
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
4c25dcd
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
d67ee16
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
d5914dc
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
4d0154e
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
2ba7001
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
0c37216
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
6938dd9
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
59be989
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
7a8d6f1
rename random_... as served_... in the CommandLineRunTimeTestCase class
ggqlq May 19, 2025
921739c
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
6d9981f
Update Lib/test/test_httpservers.py
ggqlq May 19, 2025
1d42f2c
Update Lib/test/test_httpservers.py
picnixz May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 29 additions & 22 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
import sys
import time
import urllib.parse
import argparse
import contextlib

from http import HTTPStatus

Expand Down Expand Up @@ -150,7 +152,6 @@ def server_bind(self):
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True


class HTTPSServer(HTTPServer):
def __init__(self, server_address, RequestHandlerClass,
bind_and_activate=True, *, certfile, keyfile=None,
Expand Down Expand Up @@ -1306,7 +1307,7 @@ def _get_best_family(*address):

def test(HandlerClass=BaseHTTPRequestHandler,
ServerClass=ThreadingHTTPServer,
protocol="HTTP/1.0", port=8000, bind=None,
protocol="HTTP/1.0", port=8000, bind=None, directory=None,
tls_cert=None, tls_key=None, tls_password=None):
"""Test the HTTP request handler class.

Expand All @@ -1319,6 +1320,24 @@ def test(HandlerClass=BaseHTTPRequestHandler,
if tls_cert:
server = ThreadingHTTPSServer(addr, HandlerClass, certfile=tls_cert,
keyfile=tls_key, password=tls_password)
elif ServerClass is ThreadingHTTPServer:
# ensure dual-stack is not disabled; ref #38907
class DualStackServer(ThreadingHTTPServer):
def __init__(self, server_address, RequestHandlerClass, directory=None):
super().__init__(server_address, RequestHandlerClass)
self.directory = directory

def server_bind(self):
# suppress exception when protocol is IPv4
with contextlib.suppress(Exception):
self.socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
return super().server_bind()

def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self,
directory=self.directory)
server = DualStackServer(addr, HandlerClass, directory=directory)
else:
server = ServerClass(addr, HandlerClass)

Expand All @@ -1336,10 +1355,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
print("\nKeyboard interrupt received, exiting.")
sys.exit(0)

if __name__ == '__main__':
import argparse
import contextlib

def _main(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('--cgi', action='store_true',
help='run as CGI server')
Expand All @@ -1362,7 +1378,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
parser.add_argument('port', default=8000, type=int, nargs='?',
help='bind to this port '
'(default: %(default)s)')
args = parser.parse_args()
args = parser.parse_args(args=args)

if not args.tls_cert and args.tls_key:
parser.error("--tls-key requires --tls-cert to be set")
Expand All @@ -1383,27 +1399,18 @@ def test(HandlerClass=BaseHTTPRequestHandler,
else:
handler_class = SimpleHTTPRequestHandler

# ensure dual-stack is not disabled; ref #38907
class DualStackServer(ThreadingHTTPServer):

def server_bind(self):
# suppress exception when protocol is IPv4
with contextlib.suppress(Exception):
self.socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
return super().server_bind()

def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self,
directory=args.directory)

test(
HandlerClass=handler_class,
ServerClass=DualStackServer,
ServerClass=ThreadingHTTPServer,
port=args.port,
bind=args.bind,
protocol=args.protocol,
directory=args.directory,
tls_cert=args.tls_cert,
tls_key=args.tls_key,
tls_password=tls_key_password,
)


if __name__ == '__main__':
_main()
157 changes: 156 additions & 1 deletion Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
from http import server, HTTPStatus

import http.server
import os
import socket
import sys
Expand All @@ -27,6 +28,8 @@
import threading
from unittest import mock
from io import BytesIO, StringIO
import textwrap
import contextlib

import unittest
from test import support
Expand Down Expand Up @@ -1466,7 +1469,7 @@ def test_windows_colon(self):
class MiscTestCase(unittest.TestCase):
def test_all(self):
expected = []
denylist = {'executable', 'nobody_uid', 'test'}
denylist = {'executable', 'nobody_uid', 'test', 'CommandLineServerClass'}
for name in dir(server):
if name.startswith('_') or name in denylist:
continue
Expand Down Expand Up @@ -1535,6 +1538,158 @@ def test_server_test_ipv4(self, _):
server.test(ServerClass=mock_server, bind=bind)
self.assertEqual(mock_server.address_family, socket.AF_INET)

class CommandLineTestCase(unittest.TestCase):
def setUp(self):
self.default_port = 8000
self.default_bind = None
self.default_protocol = 'HTTP/1.0'
self.default_directory = os.getcwd()
self.default_handler = SimpleHTTPRequestHandler
self.default_server = http.server.ThreadingHTTPServer
self.tls_cert = certdata_file('ssl_cert.pem')
self.tls_key = certdata_file('ssl_key.pem')
self.tls_password = 'somepass'
tls_password_file_object = tempfile.NamedTemporaryFile(mode='w+', delete=False)
tls_password_file_object.write(self.tls_password)
self.tls_password_file = tls_password_file_object.name
tls_password_file_object.close()
return super().setUp()

def tearDown(self):
if os.path.exists(self.tls_password_file):
os.remove(self.tls_password_file)
return super().tearDown()

def text_normalizer(self, string):
return textwrap.dedent(string).strip()

def invoke_httpd(self, args=[]):
output = StringIO()
with contextlib.redirect_stdout(output):
server._main(args)
return self.text_normalizer(output.getvalue())

@mock.patch('http.server.test')
def test_port_flag(self, mock_func):
ports = [8000, 65535,]
for port in ports:
with self.subTest(port=port):
self.invoke_httpd([str(port)])
mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=self.default_protocol, port=port, bind=self.default_bind, directory=self.default_directory,
tls_cert=None, tls_key=None, tls_password=None)
mock_func.reset_mock()

@mock.patch('http.server.test')
def test_directory_flag(self, mock_func):
options = ['-d', '--directory']
directories = ['.', '/foo', '\\bar', '/', 'C:\\', 'C:\\foo', 'C:\\bar',]
for flag in options:
for directory in directories:
with self.subTest(flag=flag, directory=directory):
self.invoke_httpd([flag, directory])
mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=self.default_protocol, port=self.default_port, bind=self.default_bind, directory=directory,
tls_cert=None, tls_key=None, tls_password=None)
mock_func.reset_mock()

@mock.patch('http.server.test')
def test_bind_flag(self, mock_func):
options = ['-b', '--bind']
bind_addresses = ['localhost', '127.0.0.1', '::1', '0.0.0.0', '8.8.8.8',]
for flag in options:
for bind_address in bind_addresses:
with self.subTest(flag=flag, bind_address=bind_address):
self.invoke_httpd([flag, bind_address])
mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=self.default_protocol, port=self.default_port, bind=bind_address, directory=self.default_directory,
tls_cert=None, tls_key=None, tls_password=None)
mock_func.reset_mock()

@mock.patch('http.server.test')
def test_protocol_flag(self, mock_func):
options = ['-p', '--protocol']
protocols = ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2.0', 'HTTP/3.0',]
for flag in options:
for protocol in protocols:
with self.subTest(flag=flag, protocol=protocol):
self.invoke_httpd([flag, protocol])
mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=protocol, port=self.default_port, bind=self.default_bind, directory=self.default_directory,
tls_cert=None, tls_key=None, tls_password=None)
mock_func.reset_mock()

@mock.patch('http.server.test')
def test_cgi_flag(self, mock_func):
self.invoke_httpd(['--cgi'])
mock_func.assert_called_once_with(HandlerClass=CGIHTTPRequestHandler, ServerClass=self.default_server,
protocol=self.default_protocol, port=self.default_port, bind=self.default_bind, directory=self.default_directory,
tls_cert=None, tls_key=None, tls_password=None)

@mock.patch('http.server.test')
def test_tls_flag(self, mock_func):
tls_cert_options = ['--tls-cert', ]
tls_key_options = ['--tls-key', ]
tls_password_options = ['--tls-password-file', ]
# Normal: --tls-cert and --tls-key

for tls_cert_option in tls_cert_options:
for tls_key_option in tls_key_options:
self.invoke_httpd([tls_cert_option, self.tls_cert, tls_key_option, self.tls_key])
mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=self.default_protocol, port=self.default_port, bind=self.default_bind, directory=self.default_directory,
tls_cert=self.tls_cert, tls_key=self.tls_key, tls_password=None)
mock_func.reset_mock()

# Normal: --tls-cert, --tls-key and --tls-password-file

for tls_cert_option in tls_cert_options:
for tls_key_option in tls_key_options:
for tls_password_option in tls_password_options:
self.invoke_httpd([tls_cert_option, self.tls_cert, tls_key_option, self.tls_key, tls_password_option, self.tls_password_file])

mock_func.assert_called_once_with(HandlerClass=self.default_handler, ServerClass=self.default_server,
protocol=self.default_protocol, port=self.default_port, bind=self.default_bind, directory=self.default_directory,
tls_cert=self.tls_cert, tls_key=self.tls_key, tls_password=self.tls_password)
mock_func.reset_mock()

# Abnormal: --tls-key without --tls-cert

for tls_key_option in tls_key_options:
for tls_cert_option in tls_cert_options:
with self.assertRaises(SystemExit):
self.invoke_httpd([tls_key_option, self.tls_key])
mock_func.reset_mock()

# Abnormal: --tls-password-file without --tls-cert

for tls_password_option in tls_password_options:
with self.assertRaises(SystemExit):
self.invoke_httpd([tls_password_option, self.tls_password_file])
mock_func.reset_mock()

# Abnormal: --tls-password-file cannot be opened

non_existent_file = os.path.join(tempfile.gettempdir(), os.urandom(16).hex())
retry_count = 0
while os.path.exists(non_existent_file) and retry_count < 10:
non_existent_file = os.path.join(tempfile.gettempdir(), os.urandom(16).hex())
if not os.path.exists(non_existent_file):
for tls_password_option in tls_password_options:
for tls_cert_option in tls_cert_options:
with self.assertRaises(SystemExit):
self.invoke_httpd([tls_cert_option, self.tls_cert, tls_password_option, non_existent_file])

def test_help_flag(self):
options = ['-h', '--help']
for option in options:
with self.assertRaises(SystemExit):
output = self.invoke_httpd([option])
self.assertIn('usage:', output)

def test_unknown_flag(self):
with self.assertRaises(SystemExit):
self.invoke_httpd(['--unknown-flag'])

def setUpModule():
unittest.addModuleCleanup(os.chdir, os.getcwd())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add tests for the command line interface of the ``http.server`` module.
Loading