/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/test_test_server.py

  • Committer: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010, 2011, 2016 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
import errno
 
18
import socket
 
19
try:
 
20
    import socketserver
 
21
except ImportError:
 
22
    import SocketServer as socketserver
 
23
import threading
 
24
 
 
25
 
 
26
from breezy import (
 
27
    osutils,
 
28
    tests,
 
29
    )
 
30
from breezy.tests import test_server
 
31
from breezy.tests.scenarios import load_tests_apply_scenarios
 
32
 
 
33
 
 
34
load_tests = load_tests_apply_scenarios
 
35
 
 
36
 
 
37
def portable_socket_pair():
 
38
    """Return a pair of TCP sockets connected to each other.
 
39
 
 
40
    Unlike socket.socketpair, this should work on Windows.
 
41
    """
 
42
    listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
43
    listen_sock.bind(('127.0.0.1', 0))
 
44
    listen_sock.listen(1)
 
45
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
46
    client_sock.connect(listen_sock.getsockname())
 
47
    server_sock, addr = listen_sock.accept()
 
48
    listen_sock.close()
 
49
    return server_sock, client_sock
 
50
 
 
51
 
 
52
class TCPClient(object):
 
53
 
 
54
    def __init__(self):
 
55
        self.sock = None
 
56
 
 
57
    def connect(self, addr):
 
58
        if self.sock is not None:
 
59
            raise AssertionError('Already connected to %r'
 
60
                                 % (self.sock.getsockname(),))
 
61
        self.sock = osutils.connect_socket(addr)
 
62
 
 
63
    def disconnect(self):
 
64
        if self.sock is not None:
 
65
            try:
 
66
                self.sock.shutdown(socket.SHUT_RDWR)
 
67
                self.sock.close()
 
68
            except socket.error as e:
 
69
                if e.errno in (errno.EBADF, errno.ENOTCONN, errno.ECONNRESET):
 
70
                    # Right, the socket is already down
 
71
                    pass
 
72
                else:
 
73
                    raise
 
74
            self.sock = None
 
75
 
 
76
    def write(self, s):
 
77
        return self.sock.sendall(s)
 
78
 
 
79
    def read(self, bufsize=4096):
 
80
        return self.sock.recv(bufsize)
 
81
 
 
82
 
 
83
class TCPConnectionHandler(socketserver.BaseRequestHandler):
 
84
 
 
85
    def handle(self):
 
86
        self.done = False
 
87
        self.handle_connection()
 
88
        while not self.done:
 
89
            self.handle_connection()
 
90
 
 
91
    def readline(self):
 
92
        # TODO: We should be buffering any extra data sent, etc. However, in
 
93
        #       practice, we don't send extra content, so we haven't bothered
 
94
        #       to implement it yet.
 
95
        req = self.request.recv(4096)
 
96
        # An empty string is allowed, to indicate the end of the connection
 
97
        if not req or (req.endswith(b'\n') and req.count(b'\n') == 1):
 
98
            return req
 
99
        raise ValueError('[%r] not a simple line' % (req,))
 
100
 
 
101
    def handle_connection(self):
 
102
        req = self.readline()
 
103
        if not req:
 
104
            self.done = True
 
105
        elif req == b'ping\n':
 
106
            self.request.sendall(b'pong\n')
 
107
        else:
 
108
            raise ValueError('[%s] not understood' % req)
 
109
 
 
110
 
 
111
class TestTCPServerInAThread(tests.TestCase):
 
112
 
 
113
    scenarios = [
 
114
        (name, {'server_class': getattr(test_server, name)})
 
115
        for name in
 
116
        ('TestingTCPServer', 'TestingThreadingTCPServer')]
 
117
 
 
118
    def get_server(self, server_class=None, connection_handler_class=None):
 
119
        if server_class is not None:
 
120
            self.server_class = server_class
 
121
        if connection_handler_class is None:
 
122
            connection_handler_class = TCPConnectionHandler
 
123
        server = test_server.TestingTCPServerInAThread(
 
124
            ('localhost', 0), self.server_class, connection_handler_class)
 
125
        server.start_server()
 
126
        self.addCleanup(server.stop_server)
 
127
        return server
 
128
 
 
129
    def get_client(self):
 
130
        client = TCPClient()
 
131
        self.addCleanup(client.disconnect)
 
132
        return client
 
133
 
 
134
    def get_server_connection(self, server, conn_rank):
 
135
        return server.server.clients[conn_rank]
 
136
 
 
137
    def assertClientAddr(self, client, server, conn_rank):
 
138
        conn = self.get_server_connection(server, conn_rank)
 
139
        self.assertEqual(client.sock.getsockname(), conn[1])
 
140
 
 
141
    def test_start_stop(self):
 
142
        server = self.get_server()
 
143
        client = self.get_client()
 
144
        server.stop_server()
 
145
        # since the server doesn't accept connections anymore attempting to
 
146
        # connect should fail
 
147
        client = self.get_client()
 
148
        self.assertRaises(socket.error,
 
149
                          client.connect, (server.host, server.port))
 
150
 
 
151
    def test_client_talks_server_respond(self):
 
152
        server = self.get_server()
 
153
        client = self.get_client()
 
154
        client.connect((server.host, server.port))
 
155
        self.assertIs(None, client.write(b'ping\n'))
 
156
        resp = client.read()
 
157
        self.assertClientAddr(client, server, 0)
 
158
        self.assertEqual(b'pong\n', resp)
 
159
 
 
160
    def test_server_fails_to_start(self):
 
161
        class CantStart(Exception):
 
162
            pass
 
163
 
 
164
        class CantStartServer(test_server.TestingTCPServer):
 
165
 
 
166
            def server_bind(self):
 
167
                raise CantStart()
 
168
 
 
169
        # The exception is raised in the main thread
 
170
        self.assertRaises(CantStart,
 
171
                          self.get_server, server_class=CantStartServer)
 
172
 
 
173
    def test_server_fails_while_serving_or_stopping(self):
 
174
        class CantConnect(Exception):
 
175
            pass
 
176
 
 
177
        class FailingConnectionHandler(TCPConnectionHandler):
 
178
 
 
179
            def handle(self):
 
180
                raise CantConnect()
 
181
 
 
182
        server = self.get_server(
 
183
            connection_handler_class=FailingConnectionHandler)
 
184
        # The server won't fail until a client connect
 
185
        client = self.get_client()
 
186
        client.connect((server.host, server.port))
 
187
        # We make sure the server wants to handle a request, but the request is
 
188
        # guaranteed to fail. However, the server should make sure that the
 
189
        # connection gets closed, and stop_server should then raise the
 
190
        # original exception.
 
191
        client.write(b'ping\n')
 
192
        try:
 
193
            self.assertEqual(b'', client.read())
 
194
        except socket.error as e:
 
195
            # On Windows, failing during 'handle' means we get
 
196
            # 'forced-close-of-connection'. Possibly because we haven't
 
197
            # processed the write request before we close the socket.
 
198
            WSAECONNRESET = 10054
 
199
            if e.errno in (WSAECONNRESET,):
 
200
                pass
 
201
        # Now the server has raised the exception in its own thread
 
202
        self.assertRaises(CantConnect, server.stop_server)
 
203
 
 
204
    def test_server_crash_while_responding(self):
 
205
        # We want to ensure the exception has been caught
 
206
        caught = threading.Event()
 
207
        caught.clear()
 
208
        # The thread that will serve the client, this needs to be an attribute
 
209
        # so the handler below can modify it when it's executed (it's
 
210
        # instantiated when the request is processed)
 
211
        self.connection_thread = None
 
212
 
 
213
        class FailToRespond(Exception):
 
214
            pass
 
215
 
 
216
        class FailingDuringResponseHandler(TCPConnectionHandler):
 
217
 
 
218
            # We use 'request' instead of 'self' below because the test matters
 
219
            # more and we need a container to properly set connection_thread.
 
220
            def handle_connection(request):
 
221
                request.readline()
 
222
                # Capture the thread and make it use 'caught' so we can wait on
 
223
                # the event that will be set when the exception is caught. We
 
224
                # also capture the thread to know where to look.
 
225
                self.connection_thread = threading.currentThread()
 
226
                self.connection_thread.set_sync_event(caught)
 
227
                raise FailToRespond()
 
228
 
 
229
        server = self.get_server(
 
230
            connection_handler_class=FailingDuringResponseHandler)
 
231
        client = self.get_client()
 
232
        client.connect((server.host, server.port))
 
233
        client.write(b'ping\n')
 
234
        # Wait for the exception to be caught
 
235
        caught.wait()
 
236
        self.assertEqual(b'', client.read())  # connection closed
 
237
        # Check that the connection thread did catch the exception,
 
238
        # http://pad.lv/869366 was wrongly checking the server thread which
 
239
        # works for TestingTCPServer where the connection is handled in the
 
240
        # same thread than the server one but was racy for
 
241
        # TestingThreadingTCPServer. Since the connection thread detaches
 
242
        # itself before handling the request, we are guaranteed that the
 
243
        # exception won't leak into the server thread anymore.
 
244
        self.assertRaises(FailToRespond,
 
245
                          self.connection_thread.pending_exception)
 
246
 
 
247
    def test_exception_swallowed_while_serving(self):
 
248
        # We need to ensure the exception has been caught
 
249
        caught = threading.Event()
 
250
        caught.clear()
 
251
        # The thread that will serve the client, this needs to be an attribute
 
252
        # so the handler below can access it when it's executed (it's
 
253
        # instantiated when the request is processed)
 
254
        self.connection_thread = None
 
255
 
 
256
        class CantServe(Exception):
 
257
            pass
 
258
 
 
259
        class FailingWhileServingConnectionHandler(TCPConnectionHandler):
 
260
 
 
261
            # We use 'request' instead of 'self' below because the test matters
 
262
            # more and we need a container to properly set connection_thread.
 
263
            def handle(request):
 
264
                # Capture the thread and make it use 'caught' so we can wait on
 
265
                # the event that will be set when the exception is caught. We
 
266
                # also capture the thread to know where to look.
 
267
                self.connection_thread = threading.currentThread()
 
268
                self.connection_thread.set_sync_event(caught)
 
269
                raise CantServe()
 
270
 
 
271
        server = self.get_server(
 
272
            connection_handler_class=FailingWhileServingConnectionHandler)
 
273
        self.assertEqual(True, server.server.serving)
 
274
        # Install the exception swallower
 
275
        server.set_ignored_exceptions(CantServe)
 
276
        client = self.get_client()
 
277
        # Connect to the server so the exception is raised there
 
278
        client.connect((server.host, server.port))
 
279
        # Wait for the exception to be caught
 
280
        caught.wait()
 
281
        self.assertEqual(b'', client.read())  # connection closed
 
282
        # The connection wasn't served properly but the exception should have
 
283
        # been swallowed (see test_server_crash_while_responding remark about
 
284
        # http://pad.lv/869366 explaining why we can't check the server thread
 
285
        # here). More precisely, the exception *has* been caught and captured
 
286
        # but it is cleared when joining the thread (or trying to acquire the
 
287
        # exception) and as such won't propagate to the server thread.
 
288
        self.assertIs(None, self.connection_thread.pending_exception())
 
289
        self.assertIs(None, server.pending_exception())
 
290
 
 
291
    def test_handle_request_closes_if_it_doesnt_process(self):
 
292
        server = self.get_server()
 
293
        client = self.get_client()
 
294
        server.server.serving = False
 
295
        try:
 
296
            client.connect((server.host, server.port))
 
297
            self.assertEqual(b'', client.read())
 
298
        except socket.error as e:
 
299
            if e.errno != errno.ECONNRESET:
 
300
                raise
 
301
 
 
302
 
 
303
class TestTestingSmartServer(tests.TestCase):
 
304
 
 
305
    def test_sets_client_timeout(self):
 
306
        server = test_server.TestingSmartServer(
 
307
            ('localhost', 0), None, None,
 
308
            root_client_path='/no-such-client/path')
 
309
        self.assertEqual(test_server._DEFAULT_TESTING_CLIENT_TIMEOUT,
 
310
                         server._client_timeout)
 
311
        sock = socket.socket()
 
312
        h = server._make_handler(sock)
 
313
        self.assertEqual(test_server._DEFAULT_TESTING_CLIENT_TIMEOUT,
 
314
                         h._client_timeout)
 
315
 
 
316
 
 
317
class FakeServer(object):
 
318
    """Minimal implementation to pass to TestingSmartConnectionHandler"""
 
319
    backing_transport = None
 
320
    root_client_path = '/'
 
321
 
 
322
 
 
323
class TestTestingSmartConnectionHandler(tests.TestCase):
 
324
 
 
325
    def test_connection_timeout_suppressed(self):
 
326
        self.overrideAttr(test_server, '_DEFAULT_TESTING_CLIENT_TIMEOUT', 0.01)
 
327
        s = FakeServer()
 
328
        server_sock, client_sock = portable_socket_pair()
 
329
        # This should timeout quickly, but not generate an exception.
 
330
        test_server.TestingSmartConnectionHandler(
 
331
            server_sock, server_sock.getpeername(), s)
 
332
 
 
333
    def test_connection_shutdown_while_serving_no_error(self):
 
334
        s = FakeServer()
 
335
        server_sock, client_sock = portable_socket_pair()
 
336
 
 
337
        class ShutdownConnectionHandler(
 
338
                test_server.TestingSmartConnectionHandler):
 
339
 
 
340
            def _build_protocol(self):
 
341
                self.finished = True
 
342
                return super(ShutdownConnectionHandler, self)._build_protocol()
 
343
        # This should trigger shutdown after the entering _build_protocol, and
 
344
        # we should exit cleanly, without raising an exception.
 
345
        ShutdownConnectionHandler(server_sock, server_sock.getpeername(), s)