test_asgi_application.py (1635:97afbb6c5a15) test_asgi_application.py (1654:fc7d0578e124)
1import re
2import time
3from distutils.version import LooseVersion
4
5import pytest
6
1import re
2import time
3from distutils.version import LooseVersion
4
5import pytest
6
7from conftest import option
7from conftest import skip_alert
8from unit.applications.lang.python import TestApplicationPython
9
10
11class TestASGIApplication(TestApplicationPython):
12 prerequisites = {'modules': {'python':
13 lambda v: LooseVersion(v) >= LooseVersion('3.5')}}
14 load_module = 'asgi'
15
16 def findall(self, pattern):
8from conftest import skip_alert
9from unit.applications.lang.python import TestApplicationPython
10
11
12class TestASGIApplication(TestApplicationPython):
13 prerequisites = {'modules': {'python':
14 lambda v: LooseVersion(v) >= LooseVersion('3.5')}}
15 load_module = 'asgi'
16
17 def findall(self, pattern):
17 with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f:
18 with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f:
18 return re.findall(pattern, f.read())
19
20 def test_asgi_application_variables(self):
21 self.load('variables')
22
23 body = 'Test body string.'
24
25 resp = self.http(
26 b"""POST / HTTP/1.1
27Host: localhost
28Content-Length: %d
29Custom-Header: blah
30Custom-hEader: Blah
31Content-Type: text/html
32Connection: close
33custom-header: BLAH
34
35%s""" % (len(body), body.encode()),
36 raw=True,
37 )
38
39 assert resp['status'] == 200, 'status'
40 headers = resp['headers']
41 header_server = headers.pop('Server')
42 assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
43
44 date = headers.pop('Date')
45 assert date[-4:] == ' GMT', 'date header timezone'
46 assert (
47 abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5
48 ), 'date header'
49
50 assert headers == {
51 'Connection': 'close',
52 'content-length': str(len(body)),
53 'content-type': 'text/html',
54 'request-method': 'POST',
55 'request-uri': '/',
56 'http-host': 'localhost',
57 'http-version': '1.1',
58 'custom-header': 'blah, Blah, BLAH',
59 'asgi-version': '3.0',
60 'asgi-spec-version': '2.1',
61 'scheme': 'http',
62 }, 'headers'
63 assert resp['body'] == body, 'body'
64
65 def test_asgi_application_query_string(self):
66 self.load('query_string')
67
68 resp = self.get(url='/?var1=val1&var2=val2')
69
70 assert (
71 resp['headers']['query-string'] == 'var1=val1&var2=val2'
72 ), 'query-string header'
73
74 def test_asgi_application_query_string_space(self):
75 self.load('query_string')
76
77 resp = self.get(url='/ ?var1=val1&var2=val2')
78 assert (
79 resp['headers']['query-string'] == 'var1=val1&var2=val2'
80 ), 'query-string space'
81
82 resp = self.get(url='/ %20?var1=val1&var2=val2')
83 assert (
84 resp['headers']['query-string'] == 'var1=val1&var2=val2'
85 ), 'query-string space 2'
86
87 resp = self.get(url='/ %20 ?var1=val1&var2=val2')
88 assert (
89 resp['headers']['query-string'] == 'var1=val1&var2=val2'
90 ), 'query-string space 3'
91
92 resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
93 assert (
94 resp['headers']['query-string'] == ' var1= val1 & var2=val2'
95 ), 'query-string space 4'
96
97 def test_asgi_application_query_string_empty(self):
98 self.load('query_string')
99
100 resp = self.get(url='/?')
101
102 assert resp['status'] == 200, 'query string empty status'
103 assert resp['headers']['query-string'] == '', 'query string empty'
104
105 def test_asgi_application_query_string_absent(self):
106 self.load('query_string')
107
108 resp = self.get()
109
110 assert resp['status'] == 200, 'query string absent status'
111 assert resp['headers']['query-string'] == '', 'query string absent'
112
113 @pytest.mark.skip('not yet')
114 def test_asgi_application_server_port(self):
115 self.load('server_port')
116
117 assert (
118 self.get()['headers']['Server-Port'] == '7080'
119 ), 'Server-Port header'
120
121 @pytest.mark.skip('not yet')
122 def test_asgi_application_working_directory_invalid(self):
123 self.load('empty')
124
125 assert 'success' in self.conf(
126 '"/blah"', 'applications/empty/working_directory'
127 ), 'configure invalid working_directory'
128
129 assert self.get()['status'] == 500, 'status'
130
131 def test_asgi_application_204_transfer_encoding(self):
132 self.load('204_no_content')
133
134 assert (
135 'Transfer-Encoding' not in self.get()['headers']
136 ), '204 header transfer encoding'
137
138 def test_asgi_application_shm_ack_handle(self):
139 self.load('mirror')
140
141 # Minimum possible limit
142 shm_limit = 10 * 1024 * 1024
143
144 assert (
145 'success' in self.conf('{"shm": ' + str(shm_limit) + '}',
146 'applications/mirror/limits')
147 )
148
149 # Should exceed shm_limit
150 max_body_size = 12 * 1024 * 1024
151
152 assert (
153 'success' in self.conf('{"http":{"max_body_size": '
154 + str(max_body_size) + ' }}',
155 'settings')
156 )
157
158 assert self.get()['status'] == 200, 'init'
159
160 body = '0123456789AB' * 1024 * 1024 # 12 Mb
161 resp = self.post(
162 headers={
163 'Host': 'localhost',
164 'Connection': 'close',
165 'Content-Type': 'text/html',
166 },
167 body=body,
168 read_buffer_size=1024 * 1024,
169 )
170
171 assert resp['body'] == body, 'keep-alive 1'
172
173 def test_asgi_keepalive_body(self):
174 self.load('mirror')
175
176 assert self.get()['status'] == 200, 'init'
177
178 body = '0123456789' * 500
179 (resp, sock) = self.post(
180 headers={
181 'Host': 'localhost',
182 'Connection': 'keep-alive',
183 'Content-Type': 'text/html',
184 },
185 start=True,
186 body=body,
187 read_timeout=1,
188 )
189
190 assert resp['body'] == body, 'keep-alive 1'
191
192 body = '0123456789'
193 resp = self.post(
194 headers={
195 'Host': 'localhost',
196 'Connection': 'close',
197 'Content-Type': 'text/html',
198 },
199 sock=sock,
200 body=body,
201 )
202
203 assert resp['body'] == body, 'keep-alive 2'
204
205 def test_asgi_keepalive_reconfigure(self):
206 skip_alert(
207 r'pthread_mutex.+failed',
208 r'failed to apply',
209 r'process \d+ exited on signal',
210 )
211 self.load('mirror')
212
213 assert self.get()['status'] == 200, 'init'
214
215 body = '0123456789'
216 conns = 3
217 socks = []
218
219 for i in range(conns):
220 (resp, sock) = self.post(
221 headers={
222 'Host': 'localhost',
223 'Connection': 'keep-alive',
224 'Content-Type': 'text/html',
225 },
226 start=True,
227 body=body,
228 read_timeout=1,
229 )
230
231 assert resp['body'] == body, 'keep-alive open'
232 assert 'success' in self.conf(
233 str(i + 1), 'applications/mirror/processes'
234 ), 'reconfigure'
235
236 socks.append(sock)
237
238 for i in range(conns):
239 (resp, sock) = self.post(
240 headers={
241 'Host': 'localhost',
242 'Connection': 'keep-alive',
243 'Content-Type': 'text/html',
244 },
245 start=True,
246 sock=socks[i],
247 body=body,
248 read_timeout=1,
249 )
250
251 assert resp['body'] == body, 'keep-alive request'
252 assert 'success' in self.conf(
253 str(i + 1), 'applications/mirror/processes'
254 ), 'reconfigure 2'
255
256 for i in range(conns):
257 resp = self.post(
258 headers={
259 'Host': 'localhost',
260 'Connection': 'close',
261 'Content-Type': 'text/html',
262 },
263 sock=socks[i],
264 body=body,
265 )
266
267 assert resp['body'] == body, 'keep-alive close'
268 assert 'success' in self.conf(
269 str(i + 1), 'applications/mirror/processes'
270 ), 'reconfigure 3'
271
272 def test_asgi_keepalive_reconfigure_2(self):
273 self.load('mirror')
274
275 assert self.get()['status'] == 200, 'init'
276
277 body = '0123456789'
278
279 (resp, sock) = self.post(
280 headers={
281 'Host': 'localhost',
282 'Connection': 'keep-alive',
283 'Content-Type': 'text/html',
284 },
285 start=True,
286 body=body,
287 read_timeout=1,
288 )
289
290 assert resp['body'] == body, 'reconfigure 2 keep-alive 1'
291
292 self.load('empty')
293
294 assert self.get()['status'] == 200, 'init'
295
296 (resp, sock) = self.post(
297 headers={
298 'Host': 'localhost',
299 'Connection': 'close',
300 'Content-Type': 'text/html',
301 },
302 start=True,
303 sock=sock,
304 body=body,
305 )
306
307 assert resp['status'] == 200, 'reconfigure 2 keep-alive 2'
308 assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body'
309
310 assert 'success' in self.conf(
311 {"listeners": {}, "applications": {}}
312 ), 'reconfigure 2 clear configuration'
313
314 resp = self.get(sock=sock)
315
316 assert resp == {}, 'reconfigure 2 keep-alive 3'
317
318 def test_asgi_keepalive_reconfigure_3(self):
319 self.load('empty')
320
321 assert self.get()['status'] == 200, 'init'
322
323 (_, sock) = self.http(
324 b"""GET / HTTP/1.1
325""",
326 start=True,
327 raw=True,
328 no_recv=True,
329 )
330
331 assert self.get()['status'] == 200
332
333 assert 'success' in self.conf(
334 {"listeners": {}, "applications": {}}
335 ), 'reconfigure 3 clear configuration'
336
337 resp = self.http(
338 b"""Host: localhost
339Connection: close
340
341""",
342 sock=sock,
343 raw=True,
344 )
345
346 assert resp['status'] == 200, 'reconfigure 3'
347
348 def test_asgi_process_switch(self):
349 self.load('delayed')
350
351 assert 'success' in self.conf(
352 '2', 'applications/delayed/processes'
353 ), 'configure 2 processes'
354
355 self.get(
356 headers={
357 'Host': 'localhost',
358 'Content-Length': '0',
359 'X-Delay': '5',
360 'Connection': 'close',
361 },
362 no_recv=True,
363 )
364
365 headers_delay_1 = {
366 'Connection': 'close',
367 'Host': 'localhost',
368 'Content-Length': '0',
369 'X-Delay': '1',
370 }
371
372 self.get(headers=headers_delay_1, no_recv=True)
373
374 time.sleep(0.5)
375
376 for _ in range(10):
377 self.get(headers=headers_delay_1, no_recv=True)
378
379 self.get(headers=headers_delay_1)
380
381 def test_asgi_application_loading_error(self):
382 skip_alert(r'Python failed to import module "blah"')
383
384 self.load('empty')
385
386 assert 'success' in self.conf('"blah"', 'applications/empty/module')
387
388 assert self.get()['status'] == 503, 'loading error'
389
390 def test_asgi_application_threading(self):
391 """wait_for_record() timeouts after 5s while every thread works at
392 least 3s. So without releasing GIL test should fail.
393 """
394
395 self.load('threading')
396
397 for _ in range(10):
398 self.get(no_recv=True)
399
400 assert (
401 self.wait_for_record(r'\(5\) Thread: 100') is not None
402 ), 'last thread finished'
19 return re.findall(pattern, f.read())
20
21 def test_asgi_application_variables(self):
22 self.load('variables')
23
24 body = 'Test body string.'
25
26 resp = self.http(
27 b"""POST / HTTP/1.1
28Host: localhost
29Content-Length: %d
30Custom-Header: blah
31Custom-hEader: Blah
32Content-Type: text/html
33Connection: close
34custom-header: BLAH
35
36%s""" % (len(body), body.encode()),
37 raw=True,
38 )
39
40 assert resp['status'] == 200, 'status'
41 headers = resp['headers']
42 header_server = headers.pop('Server')
43 assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
44
45 date = headers.pop('Date')
46 assert date[-4:] == ' GMT', 'date header timezone'
47 assert (
48 abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5
49 ), 'date header'
50
51 assert headers == {
52 'Connection': 'close',
53 'content-length': str(len(body)),
54 'content-type': 'text/html',
55 'request-method': 'POST',
56 'request-uri': '/',
57 'http-host': 'localhost',
58 'http-version': '1.1',
59 'custom-header': 'blah, Blah, BLAH',
60 'asgi-version': '3.0',
61 'asgi-spec-version': '2.1',
62 'scheme': 'http',
63 }, 'headers'
64 assert resp['body'] == body, 'body'
65
66 def test_asgi_application_query_string(self):
67 self.load('query_string')
68
69 resp = self.get(url='/?var1=val1&var2=val2')
70
71 assert (
72 resp['headers']['query-string'] == 'var1=val1&var2=val2'
73 ), 'query-string header'
74
75 def test_asgi_application_query_string_space(self):
76 self.load('query_string')
77
78 resp = self.get(url='/ ?var1=val1&var2=val2')
79 assert (
80 resp['headers']['query-string'] == 'var1=val1&var2=val2'
81 ), 'query-string space'
82
83 resp = self.get(url='/ %20?var1=val1&var2=val2')
84 assert (
85 resp['headers']['query-string'] == 'var1=val1&var2=val2'
86 ), 'query-string space 2'
87
88 resp = self.get(url='/ %20 ?var1=val1&var2=val2')
89 assert (
90 resp['headers']['query-string'] == 'var1=val1&var2=val2'
91 ), 'query-string space 3'
92
93 resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
94 assert (
95 resp['headers']['query-string'] == ' var1= val1 & var2=val2'
96 ), 'query-string space 4'
97
98 def test_asgi_application_query_string_empty(self):
99 self.load('query_string')
100
101 resp = self.get(url='/?')
102
103 assert resp['status'] == 200, 'query string empty status'
104 assert resp['headers']['query-string'] == '', 'query string empty'
105
106 def test_asgi_application_query_string_absent(self):
107 self.load('query_string')
108
109 resp = self.get()
110
111 assert resp['status'] == 200, 'query string absent status'
112 assert resp['headers']['query-string'] == '', 'query string absent'
113
114 @pytest.mark.skip('not yet')
115 def test_asgi_application_server_port(self):
116 self.load('server_port')
117
118 assert (
119 self.get()['headers']['Server-Port'] == '7080'
120 ), 'Server-Port header'
121
122 @pytest.mark.skip('not yet')
123 def test_asgi_application_working_directory_invalid(self):
124 self.load('empty')
125
126 assert 'success' in self.conf(
127 '"/blah"', 'applications/empty/working_directory'
128 ), 'configure invalid working_directory'
129
130 assert self.get()['status'] == 500, 'status'
131
132 def test_asgi_application_204_transfer_encoding(self):
133 self.load('204_no_content')
134
135 assert (
136 'Transfer-Encoding' not in self.get()['headers']
137 ), '204 header transfer encoding'
138
139 def test_asgi_application_shm_ack_handle(self):
140 self.load('mirror')
141
142 # Minimum possible limit
143 shm_limit = 10 * 1024 * 1024
144
145 assert (
146 'success' in self.conf('{"shm": ' + str(shm_limit) + '}',
147 'applications/mirror/limits')
148 )
149
150 # Should exceed shm_limit
151 max_body_size = 12 * 1024 * 1024
152
153 assert (
154 'success' in self.conf('{"http":{"max_body_size": '
155 + str(max_body_size) + ' }}',
156 'settings')
157 )
158
159 assert self.get()['status'] == 200, 'init'
160
161 body = '0123456789AB' * 1024 * 1024 # 12 Mb
162 resp = self.post(
163 headers={
164 'Host': 'localhost',
165 'Connection': 'close',
166 'Content-Type': 'text/html',
167 },
168 body=body,
169 read_buffer_size=1024 * 1024,
170 )
171
172 assert resp['body'] == body, 'keep-alive 1'
173
174 def test_asgi_keepalive_body(self):
175 self.load('mirror')
176
177 assert self.get()['status'] == 200, 'init'
178
179 body = '0123456789' * 500
180 (resp, sock) = self.post(
181 headers={
182 'Host': 'localhost',
183 'Connection': 'keep-alive',
184 'Content-Type': 'text/html',
185 },
186 start=True,
187 body=body,
188 read_timeout=1,
189 )
190
191 assert resp['body'] == body, 'keep-alive 1'
192
193 body = '0123456789'
194 resp = self.post(
195 headers={
196 'Host': 'localhost',
197 'Connection': 'close',
198 'Content-Type': 'text/html',
199 },
200 sock=sock,
201 body=body,
202 )
203
204 assert resp['body'] == body, 'keep-alive 2'
205
206 def test_asgi_keepalive_reconfigure(self):
207 skip_alert(
208 r'pthread_mutex.+failed',
209 r'failed to apply',
210 r'process \d+ exited on signal',
211 )
212 self.load('mirror')
213
214 assert self.get()['status'] == 200, 'init'
215
216 body = '0123456789'
217 conns = 3
218 socks = []
219
220 for i in range(conns):
221 (resp, sock) = self.post(
222 headers={
223 'Host': 'localhost',
224 'Connection': 'keep-alive',
225 'Content-Type': 'text/html',
226 },
227 start=True,
228 body=body,
229 read_timeout=1,
230 )
231
232 assert resp['body'] == body, 'keep-alive open'
233 assert 'success' in self.conf(
234 str(i + 1), 'applications/mirror/processes'
235 ), 'reconfigure'
236
237 socks.append(sock)
238
239 for i in range(conns):
240 (resp, sock) = self.post(
241 headers={
242 'Host': 'localhost',
243 'Connection': 'keep-alive',
244 'Content-Type': 'text/html',
245 },
246 start=True,
247 sock=socks[i],
248 body=body,
249 read_timeout=1,
250 )
251
252 assert resp['body'] == body, 'keep-alive request'
253 assert 'success' in self.conf(
254 str(i + 1), 'applications/mirror/processes'
255 ), 'reconfigure 2'
256
257 for i in range(conns):
258 resp = self.post(
259 headers={
260 'Host': 'localhost',
261 'Connection': 'close',
262 'Content-Type': 'text/html',
263 },
264 sock=socks[i],
265 body=body,
266 )
267
268 assert resp['body'] == body, 'keep-alive close'
269 assert 'success' in self.conf(
270 str(i + 1), 'applications/mirror/processes'
271 ), 'reconfigure 3'
272
273 def test_asgi_keepalive_reconfigure_2(self):
274 self.load('mirror')
275
276 assert self.get()['status'] == 200, 'init'
277
278 body = '0123456789'
279
280 (resp, sock) = self.post(
281 headers={
282 'Host': 'localhost',
283 'Connection': 'keep-alive',
284 'Content-Type': 'text/html',
285 },
286 start=True,
287 body=body,
288 read_timeout=1,
289 )
290
291 assert resp['body'] == body, 'reconfigure 2 keep-alive 1'
292
293 self.load('empty')
294
295 assert self.get()['status'] == 200, 'init'
296
297 (resp, sock) = self.post(
298 headers={
299 'Host': 'localhost',
300 'Connection': 'close',
301 'Content-Type': 'text/html',
302 },
303 start=True,
304 sock=sock,
305 body=body,
306 )
307
308 assert resp['status'] == 200, 'reconfigure 2 keep-alive 2'
309 assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body'
310
311 assert 'success' in self.conf(
312 {"listeners": {}, "applications": {}}
313 ), 'reconfigure 2 clear configuration'
314
315 resp = self.get(sock=sock)
316
317 assert resp == {}, 'reconfigure 2 keep-alive 3'
318
319 def test_asgi_keepalive_reconfigure_3(self):
320 self.load('empty')
321
322 assert self.get()['status'] == 200, 'init'
323
324 (_, sock) = self.http(
325 b"""GET / HTTP/1.1
326""",
327 start=True,
328 raw=True,
329 no_recv=True,
330 )
331
332 assert self.get()['status'] == 200
333
334 assert 'success' in self.conf(
335 {"listeners": {}, "applications": {}}
336 ), 'reconfigure 3 clear configuration'
337
338 resp = self.http(
339 b"""Host: localhost
340Connection: close
341
342""",
343 sock=sock,
344 raw=True,
345 )
346
347 assert resp['status'] == 200, 'reconfigure 3'
348
349 def test_asgi_process_switch(self):
350 self.load('delayed')
351
352 assert 'success' in self.conf(
353 '2', 'applications/delayed/processes'
354 ), 'configure 2 processes'
355
356 self.get(
357 headers={
358 'Host': 'localhost',
359 'Content-Length': '0',
360 'X-Delay': '5',
361 'Connection': 'close',
362 },
363 no_recv=True,
364 )
365
366 headers_delay_1 = {
367 'Connection': 'close',
368 'Host': 'localhost',
369 'Content-Length': '0',
370 'X-Delay': '1',
371 }
372
373 self.get(headers=headers_delay_1, no_recv=True)
374
375 time.sleep(0.5)
376
377 for _ in range(10):
378 self.get(headers=headers_delay_1, no_recv=True)
379
380 self.get(headers=headers_delay_1)
381
382 def test_asgi_application_loading_error(self):
383 skip_alert(r'Python failed to import module "blah"')
384
385 self.load('empty')
386
387 assert 'success' in self.conf('"blah"', 'applications/empty/module')
388
389 assert self.get()['status'] == 503, 'loading error'
390
391 def test_asgi_application_threading(self):
392 """wait_for_record() timeouts after 5s while every thread works at
393 least 3s. So without releasing GIL test should fail.
394 """
395
396 self.load('threading')
397
398 for _ in range(10):
399 self.get(no_recv=True)
400
401 assert (
402 self.wait_for_record(r'\(5\) Thread: 100') is not None
403 ), 'last thread finished'