OLD | NEW |
| (Empty) |
1 # Copyright (c) 2006 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 """ | |
5 Whitebox tests for TCP APIs. | |
6 """ | |
7 | |
8 import errno, socket, os | |
9 | |
10 try: | |
11 import resource | |
12 except ImportError: | |
13 resource = None | |
14 | |
15 from twisted.trial.unittest import TestCase | |
16 | |
17 from twisted.python import log | |
18 from twisted.internet.tcp import ECONNABORTED, ENOMEM, ENFILE, EMFILE, ENOBUFS,
EINPROGRESS, Port | |
19 from twisted.internet.protocol import ServerFactory | |
20 from twisted.python.runtime import platform | |
21 from twisted.internet.defer import maybeDeferred, gatherResults | |
22 from twisted.internet import reactor, interfaces | |
23 | |
24 | |
25 class PlatformAssumptionsTestCase(TestCase): | |
26 """ | |
27 Test assumptions about platform behaviors. | |
28 """ | |
29 socketLimit = 8192 | |
30 | |
31 def setUp(self): | |
32 self.openSockets = [] | |
33 if resource is not None: | |
34 self.originalFileLimit = resource.getrlimit(resource.RLIMIT_NOFILE) | |
35 resource.setrlimit(resource.RLIMIT_NOFILE, (128, self.originalFileLi
mit[1])) | |
36 self.socketLimit = 256 | |
37 | |
38 | |
39 def tearDown(self): | |
40 while self.openSockets: | |
41 self.openSockets.pop().close() | |
42 if resource is not None: | |
43 # OS X implicitly lowers the hard limit in the setrlimit call | |
44 # above. Retrieve the new hard limit to pass in to this | |
45 # setrlimit call, so that it doesn't give us a permission denied | |
46 # error. | |
47 currentHardLimit = resource.getrlimit(resource.RLIMIT_NOFILE)[1] | |
48 newSoftLimit = min(self.originalFileLimit[0], currentHardLimit) | |
49 resource.setrlimit(resource.RLIMIT_NOFILE, (newSoftLimit, currentHar
dLimit)) | |
50 | |
51 | |
52 def socket(self): | |
53 """ | |
54 Create and return a new socket object, also tracking it so it can be | |
55 closed in the test tear down. | |
56 """ | |
57 s = socket.socket() | |
58 self.openSockets.append(s) | |
59 return s | |
60 | |
61 | |
62 def test_acceptOutOfFiles(self): | |
63 """ | |
64 Test that the platform accept(2) call fails with either L{EMFILE} or | |
65 L{ENOBUFS} when there are too many file descriptors open. | |
66 """ | |
67 # Make a server to which to connect | |
68 port = self.socket() | |
69 port.bind(('127.0.0.1', 0)) | |
70 serverPortNumber = port.getsockname()[1] | |
71 port.listen(5) | |
72 | |
73 # Use up all the file descriptors | |
74 for i in xrange(self.socketLimit): | |
75 try: | |
76 self.socket() | |
77 except socket.error, e: | |
78 if e.args[0] in (EMFILE, ENOBUFS): | |
79 self.openSockets.pop().close() | |
80 break | |
81 else: | |
82 raise | |
83 else: | |
84 self.fail("Could provoke neither EMFILE nor ENOBUFS from platform.") | |
85 | |
86 # Make a client to use to connect to the server | |
87 client = self.socket() | |
88 client.setblocking(False) | |
89 | |
90 # Non-blocking connect is supposed to fail, but this is not true | |
91 # everywhere (e.g. freeBSD) | |
92 self.assertIn(client.connect_ex(('127.0.0.1', serverPortNumber)), | |
93 (0, EINPROGRESS)) | |
94 | |
95 # Make sure that the accept call fails in the way we expect. | |
96 exc = self.assertRaises(socket.error, port.accept) | |
97 self.assertIn(exc.args[0], (EMFILE, ENOBUFS)) | |
98 if platform.getType() == "win32": | |
99 test_acceptOutOfFiles.skip = ( | |
100 "Windows requires an unacceptably large amount of resources to " | |
101 "provoke this behavior in the naive manner.") | |
102 | |
103 | |
104 | |
105 class SelectReactorTestCase(TestCase): | |
106 """ | |
107 Tests for select-specific failure conditions. | |
108 """ | |
109 | |
110 def setUp(self): | |
111 self.ports = [] | |
112 self.messages = [] | |
113 log.addObserver(self.messages.append) | |
114 | |
115 | |
116 def tearDown(self): | |
117 log.removeObserver(self.messages.append) | |
118 return gatherResults([ | |
119 maybeDeferred(p.stopListening) | |
120 for p in self.ports]) | |
121 | |
122 | |
123 def port(self, portNumber, factory, interface): | |
124 """ | |
125 Create, start, and return a new L{Port}, also tracking it so it can | |
126 be stopped in the test tear down. | |
127 """ | |
128 p = Port(portNumber, factory, interface=interface) | |
129 p.startListening() | |
130 self.ports.append(p) | |
131 return p | |
132 | |
133 | |
134 def _acceptFailureTest(self, socketErrorNumber): | |
135 """ | |
136 Test behavior in the face of an exception from C{accept(2)}. | |
137 | |
138 On any exception which indicates the platform is unable or unwilling | |
139 to allocate further resources to us, the existing port should remain | |
140 listening, a message should be logged, and the exception should not | |
141 propagate outward from doRead. | |
142 | |
143 @param socketErrorNumber: The errno to simulate from accept. | |
144 """ | |
145 class FakeSocket(object): | |
146 """ | |
147 Pretend to be a socket in an overloaded system. | |
148 """ | |
149 def accept(self): | |
150 raise socket.error( | |
151 socketErrorNumber, os.strerror(socketErrorNumber)) | |
152 | |
153 factory = ServerFactory() | |
154 port = self.port(0, factory, interface='127.0.0.1') | |
155 originalSocket = port.socket | |
156 try: | |
157 port.socket = FakeSocket() | |
158 | |
159 port.doRead() | |
160 | |
161 expectedFormat = "Could not accept new connection (%s)" | |
162 expectedErrorCode = errno.errorcode[socketErrorNumber] | |
163 expectedMessage = expectedFormat % (expectedErrorCode,) | |
164 for msg in self.messages: | |
165 if msg.get('message') == (expectedMessage,): | |
166 break | |
167 else: | |
168 self.fail("Log event for failed accept not found in " | |
169 "%r" % (self.messages,)) | |
170 finally: | |
171 port.socket = originalSocket | |
172 | |
173 | |
174 def test_tooManyFilesFromAccept(self): | |
175 """ | |
176 C{accept(2)} can fail with C{EMFILE} when there are too many open file | |
177 descriptors in the process. Test that this doesn't negatively impact | |
178 any other existing connections. | |
179 | |
180 C{EMFILE} mainly occurs on Linux when the open file rlimit is | |
181 encountered. | |
182 """ | |
183 return self._acceptFailureTest(EMFILE) | |
184 | |
185 | |
186 def test_noBufferSpaceFromAccept(self): | |
187 """ | |
188 Similar to L{test_tooManyFilesFromAccept}, but test the case where | |
189 C{accept(2)} fails with C{ENOBUFS}. | |
190 | |
191 This mainly occurs on Windows and FreeBSD, but may be possible on | |
192 Linux and other platforms as well. | |
193 """ | |
194 return self._acceptFailureTest(ENOBUFS) | |
195 | |
196 | |
197 def test_connectionAbortedFromAccept(self): | |
198 """ | |
199 Similar to L{test_tooManyFilesFromAccept}, but test the case where | |
200 C{accept(2)} fails with C{ECONNABORTED}. | |
201 | |
202 It is not clear whether this is actually possible for TCP | |
203 connections on modern versions of Linux. | |
204 """ | |
205 return self._acceptFailureTest(ECONNABORTED) | |
206 | |
207 | |
208 def test_noFilesFromAccept(self): | |
209 """ | |
210 Similar to L{test_tooManyFilesFromAccept}, but test the case where | |
211 C{accept(2)} fails with C{ENFILE}. | |
212 | |
213 This can occur on Linux when the system has exhausted (!) its supply | |
214 of inodes. | |
215 """ | |
216 return self._acceptFailureTest(ENFILE) | |
217 if platform.getType() == 'win32': | |
218 test_noFilesFromAccept.skip = "Windows accept(2) cannot generate ENFILE" | |
219 | |
220 | |
221 def test_noMemoryFromAccept(self): | |
222 """ | |
223 Similar to L{test_tooManyFilesFromAccept}, but test the case where | |
224 C{accept(2)} fails with C{ENOMEM}. | |
225 | |
226 On Linux at least, this can sensibly occur, even in a Python program | |
227 (which eats memory like no ones business), when memory has become | |
228 fragmented or low memory has been filled (d_alloc calls | |
229 kmem_cache_alloc calls kmalloc - kmalloc only allocates out of low | |
230 memory). | |
231 """ | |
232 return self._acceptFailureTest(ENOMEM) | |
233 if platform.getType() == 'win32': | |
234 test_noMemoryFromAccept.skip = "Windows accept(2) cannot generate ENOMEM
" | |
235 | |
236 if not interfaces.IReactorFDSet.providedBy(reactor): | |
237 skipMsg = 'This test only applies to reactors that implement IReactorFDset' | |
238 PlatformAssumptionsTestCase.skip = skipMsg | |
239 SelectReactorTestCase.skip = skipMsg | |
240 | |
OLD | NEW |