blob: 3add41d8bf4a7e1a2101b931c6723c9437c8a311 [file] [edit]
#!/usr/bin/env vpython
# Copyright 2013 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import re
import threading
import time
import unittest
import platform
from six.moves import BaseHTTPServer
from six.moves import socketserver
# Mutates sys.path.
import test_env
# third_party/
from depot_tools import auto_stub
from utils import authenticators
from utils import net
class SleepingServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
"""Multithreaded server that serves requests that block at various stages."""
# Lingering keep-alive HTTP connections keep (not very smart) HTTPServer
# threads alive as well. Convert them to daemon threads so that they don't
# block process exit.
daemon_threads = True
def __init__(self):
BaseHTTPServer.HTTPServer.__init__(self, ("127.0.0.1", 0), SleepingHandler)
self.dying = False
self.dying_cv = threading.Condition()
self.serving_thread = None
def handle_error(self, _request, _client_address):
# Mute "error: [Errno 32] Broken pipe" errors.
pass
def start(self):
self.serving_thread = threading.Thread(
target=self.serve_forever, kwargs={"poll_interval": 0.05}
)
self.serving_thread.start()
def stop(self):
with self.dying_cv:
self.dying = True
self.dying_cv.notifyAll()
self.shutdown()
@property
def url(self):
return "http://%s:%d" % self.socket.getsockname()
def sleep(self, timeout):
deadline = time.time() + timeout
with self.dying_cv:
while not self.dying and time.time() < deadline:
self.dying_cv.wait(deadline - time.time())
class SleepingHandler(BaseHTTPServer.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
path_re = re.compile(r"/(.*)/([\.\d]*)(\?.*)?")
first_line = "FIRST LINE\n"
second_line = "SECOND LINE\n"
full_response = first_line + second_line
modes = {
"sleep_before_response": ["SLEEP", "HEADERS", "FIRST", "SECOND"],
"sleep_after_headers": ["HEADERS", "SLEEP", "FIRST", "SECOND"],
"sleep_during_response": ["HEADERS", "FIRST", "SLEEP", "SECOND"],
"sleep_after_response": ["HEADERS", "FIRST", "SECOND", "SLEEP"],
}
def send_headers(self):
self.send_response(200)
self.send_header("Content-Length", len(self.full_response))
self.end_headers()
def log_message(self, _format, *_args):
# Mute "GET /sleep_before_response/0.000000 HTTP/1.1" 200 -" messages.
pass
def do_GET(self):
# Split request string like '/sleep/0.1?param=1' into ('sleep', 0.1) pair.
match = self.path_re.match(self.path)
if not match:
self.send_error(404)
return
mode, timeout, _ = match.groups()
# Ensure timeout is float.
try:
timeout = float(timeout)
except ValueError:
self.send_error(400)
return
# Ensure mode is known.
if mode not in self.modes:
self.send_error(404)
return
# Mapping mode's action -> function to call.
actions = {
"SLEEP": lambda: self.server.sleep(timeout),
"HEADERS": self.send_headers,
"FIRST": lambda: self.wfile.write(self.first_line),
"SECOND": lambda: self.wfile.write(self.second_line),
}
# Execute all actions defined by the mode.
for action in self.modes[mode]:
actions[action]()
class UrlOpenTimeoutTest(auto_stub.TestCase):
def setUp(self):
super(UrlOpenTimeoutTest, self).setUp()
self.mock(authenticators, "OAuthAuthenticator", lambda *_: None)
self.server = SleepingServer()
self.server.start()
def tearDown(self):
self.server.stop()
self.server.server_close()
self.server = None
super(UrlOpenTimeoutTest, self).tearDown()
def call(self, mode, sleep_duration, **kwargs):
url = self.server.url + "/%s/%f" % (mode, sleep_duration)
kwargs["max_attempts"] = 2
return net.url_open(url, **kwargs)
def test_urlopen_success(self):
# Server doesn't block.
for mode in SleepingHandler.modes:
self.assertEqual(
self.call(mode, 0, read_timeout=0.1).read(),
SleepingHandler.full_response,
)
# Server does block, but url_open called without read timeout.
for mode in SleepingHandler.modes:
self.assertEqual(
self.call(mode, 0.25, read_timeout=None).read(),
SleepingHandler.full_response,
)
def test_urlopen_retry(self):
# This should trigger retry logic and eventually return None.
self.mock(net, "sleep_before_retry", lambda *_: None)
stream = self.call("sleep_before_response", 0.25, read_timeout=0.1)
self.assertIsNone(stream)
def test_urlopen_keeping_connection(self):
# Sleeping after request is sent -> it's just connection keep alive.
stream = self.call("sleep_after_response", 0.25, read_timeout=0.1)
self.assertEqual(stream.read(), SleepingHandler.full_response)
def test_urlopen_timeout_early_stream(self):
# Timeouts while reading from the stream.
stream = self.call("sleep_after_headers", 0.25, read_timeout=0.1)
self.assertTrue(stream)
gen = stream.iter_content(len(SleepingHandler.first_line))
with self.assertRaises(net.TimeoutError):
next(gen)
@unittest.skipIf(
platform.system() == "Darwin",
"TODO(crbug.com/1017545):AssertionError: TimeoutError not raised",
)
def test_urlopen_timeout_mid_stream(self):
# Timeouts while reading from the stream.
stream = self.call("sleep_during_response", 0.25, read_timeout=0.1)
self.assertTrue(stream)
gen = stream.iter_content(len(SleepingHandler.first_line))
next(gen)
with self.assertRaises(net.TimeoutError):
next(gen)
if __name__ == "__main__":
test_env.main()