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

Open
wants to merge 66 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 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
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
8 changes: 6 additions & 2 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
sys.exit(0)


if __name__ == '__main__':
def _main(args=None):
import argparse
import contextlib

Expand All @@ -1021,7 +1021,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)

if not args.tls_cert and args.tls_key:
parser.error("--tls-key requires --tls-cert to be set")
Expand Down Expand Up @@ -1061,3 +1061,7 @@ def finish_request(self, request, client_address):
tls_key=args.tls_key,
tls_password=tls_key_password,
)


if __name__ == '__main__':
_main()
253 changes: 253 additions & 0 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
SimpleHTTPRequestHandler
from http import server, HTTPStatus

import contextlib
import os
import socket
import subprocess
import sys
import re
import ntpath
Expand All @@ -20,6 +22,7 @@
import html
import http, http.client
import urllib.parse
import urllib.request
import tempfile
import time
import datetime
Expand All @@ -32,6 +35,8 @@
from test.support import (
is_apple, import_helper, os_helper, threading_helper
)
from test.support.script_helper import kill_python, spawn_python
from test.support.socket_helper import find_unused_port

try:
import ssl
Expand Down Expand Up @@ -1280,6 +1285,254 @@ def test_server_test_ipv4(self, _):
self.assertEqual(mock_server.address_family, socket.AF_INET)


class CommandLineTestCase(unittest.TestCase):
default_port = 8000
default_bind = None
default_protocol = 'HTTP/1.0'
default_handler = SimpleHTTPRequestHandler
default_server = unittest.mock.ANY
tls_cert = certdata_file('ssl_cert.pem')
tls_key = certdata_file('ssl_key.pem')
tls_password = 'somepass'
tls_cert_options = ['--tls-cert']
tls_key_options = ['--tls-key']
tls_password_options = ['--tls-password-file']
args = {
'HandlerClass': default_handler,
'ServerClass': default_server,
'protocol': default_protocol,
'port': default_port,
'bind': default_bind,
'tls_cert': None,
'tls_key': None,
'tls_password': None,
}

def setUp(self):
super().setUp()
self.tls_password_file = tempfile.mktemp()
with open(self.tls_password_file, 'wb') as f:
f.write(self.tls_password.encode())
self.addCleanup(os_helper.unlink, self.tls_password_file)

def invoke_httpd(self, *args, stdout=None, stderr=None):
stdout = StringIO() if stdout is None else stdout
stderr = StringIO() if stderr is None else stderr
with contextlib.redirect_stdout(stdout), \
contextlib.redirect_stderr(stderr):
server._main(args)
return stdout.getvalue(), stderr.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))
call_args = self.args | dict(port=port)
mock_func.assert_called_once_with(**call_args)
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',
'/home/user', './foo/foo2', 'D:\\foo\\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(**self.args)
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)
call_args = self.args | dict(bind=bind_address)
mock_func.assert_called_once_with(**call_args)
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)
call_args = self.args | dict(protocol=protocol)
mock_func.assert_called_once_with(**call_args)
mock_func.reset_mock()

@unittest.skipIf(ssl is None, "requires ssl")
@mock.patch('http.server.test')
def test_tls_cert_and_key_flags(self, mock_func):
for tls_cert_option in self.tls_cert_options:
for tls_key_option in self.tls_key_options:
self.invoke_httpd(tls_cert_option, self.tls_cert,
tls_key_option, self.tls_key)
call_args = {
'tls_cert': self.tls_cert,
'tls_key': self.tls_key,
}
call_args = self.args | call_args
mock_func.assert_called_once_with(**call_args)
mock_func.reset_mock()

@unittest.skipIf(ssl is None, "requires ssl")
@mock.patch('http.server.test')
def test_tls_cert_and_key_and_password_flags(self, mock_func):
for tls_cert_option in self.tls_cert_options:
for tls_key_option in self.tls_key_options:
for tls_password_option in self.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)
call_args = {
'tls_cert': self.tls_cert,
'tls_key': self.tls_key,
'tls_password': self.tls_password,
}
call_args = self.args | call_args
mock_func.assert_called_once_with(**call_args)
mock_func.reset_mock()

@unittest.skipIf(ssl is None, "requires ssl")
@mock.patch('http.server.test')
def test_missing_tls_cert_flag(self, mock_func):
for tls_key_option in self.tls_key_options:
with self.assertRaises(SystemExit):
self.invoke_httpd(tls_key_option, self.tls_key)
mock_func.reset_mock()

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

@unittest.skipIf(ssl is None, "requires ssl")
@mock.patch('http.server.test')
def test_invalid_password_file(self, mock_func):
non_existent_file = 'non_existent_file'
for tls_password_option in self.tls_password_options:
for tls_cert_option in self.tls_cert_options:
with self.assertRaises(SystemExit):
self.invoke_httpd(tls_cert_option,
self.tls_cert,
tls_password_option,
non_existent_file)

@mock.patch('http.server.test')
def test_no_arguments(self, mock_func):
self.invoke_httpd()
mock_func.assert_called_once_with(**self.args)
mock_func.reset_mock()

@mock.patch('http.server.test')
def test_help_flag(self, _):
options = ['-h', '--help']
stdout, stderr = StringIO(), StringIO()
for option in options:
with self.assertRaises(SystemExit):
self.invoke_httpd(option, stdout=stdout, stderr=stderr)
self.assertIn('usage', stdout.getvalue())
self.assertEqual('', stderr.getvalue())

@mock.patch('http.server.test')
def test_unknown_flag(self, _):
stdout, stderr = StringIO(), StringIO()
with self.assertRaises(SystemExit):
self.invoke_httpd('--unknown-flag', stdout=stdout, stderr=stderr)
self.assertEqual('', stdout.getvalue())
self.assertIn('error', stderr.getvalue())

class CommandLineRunTimeTestCase(unittest.TestCase):
random_data = os.urandom(32)
random_file_name = 'served_filename'
tls_cert = certdata_file('ssl_cert.pem')
tls_key = certdata_file('ssl_key.pem')
tls_password = 'somepass'

def setUp(self):
super().setUp()
with open(self.random_file_name, 'wb') as f:
f.write(self.random_data)
self.addCleanup(os_helper.unlink, self.random_file_name)
self.tls_password_file = tempfile.mktemp()
with open(self.tls_password_file, 'wb') as f:
f.write(self.tls_password.encode())
self.addCleanup(os_helper.unlink, self.tls_password_file)

def fetch_file(self, path, allow_self_signed_cert=True):
context = ssl.create_default_context()
if allow_self_signed_cert:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(path, method='GET')
with urllib.request.urlopen(req, context=context) as res:
return res.read()

def parse_cli_output(self, output):
matches = re.search(r'\((https?)://([^/:]+):(\d+)/?\)', output)
if matches is None:
return None, None, None
return matches.group(1), matches.group(2), int(matches.group(3))

def wait_for_server(self, proc, protocol, port, bind, timeout=50):
"""Parses the output of the server process by lines and returns True if
the server is listening on the given port and bind address."""
while timeout > 0:
line = proc.stdout.readline()
if not line:
time.sleep(0.1)
timeout -= 1
continue
protocol_, host_, port_ = self.parse_cli_output(line)
if not protocol_ or not host_ or not port_:
time.sleep(0.1)
timeout -= 1
continue
if protocol_ == protocol and host_ == bind and port_ == port:
return True
else:
break
return False

def test_http_client(self):
port = find_unused_port()
bind = '127.0.0.1'
proc = spawn_python('-u', '-m', 'http.server', str(port), '-b', bind,
bufsize=1, text=True)
self.addCleanup(kill_python, proc)
self.addCleanup(proc.terminate)
self.assertTrue(self.wait_for_server(proc, 'http', port, bind))
res = self.fetch_file(f'http://{bind}:{port}/{self.random_file_name}')
self.assertEqual(res, self.random_data)

def test_https_client(self):
port = find_unused_port()
bind = '127.0.0.1'
proc = spawn_python('-u', '-m', 'http.server', str(port), '-b', bind,
'--tls-cert', self.tls_cert,
'--tls-key', self.tls_key,
'--tls-password-file', self.tls_password_file,
bufsize=1, text=True)
self.addCleanup(kill_python, proc)
self.addCleanup(proc.terminate)
self.assertTrue(self.wait_for_server(proc, 'https', port, bind))
res = self.fetch_file(f'https://{bind}:{port}/{self.random_file_name}')
self.assertEqual(res, self.random_data)

def setUpModule():
unittest.addModuleCleanup(os.chdir, os.getcwd())

Expand Down
Loading