xref: /unit/test/test_python_application.py (revision 1283:e7d0b112241e)
1import re
2import time
3import unittest
4from unit.applications.lang.python import TestApplicationPython
5
6
7class TestPythonApplication(TestApplicationPython):
8    prerequisites = {'modules': ['python']}
9
10    def findall(self, pattern):
11        with open(self.testdir + '/unit.log', 'r', errors='ignore') as f:
12            return re.findall(pattern, f.read())
13
14    def test_python_application_variables(self):
15        self.load('variables')
16
17        body = 'Test body string.'
18
19        resp = self.post(
20            headers={
21                'Host': 'localhost',
22                'Content-Type': 'text/html',
23                'Custom-Header': 'blah',
24                'Connection': 'close',
25            },
26            body=body,
27        )
28
29        self.assertEqual(resp['status'], 200, 'status')
30        headers = resp['headers']
31        header_server = headers.pop('Server')
32        self.assertRegex(header_server, r'Unit/[\d\.]+', 'server header')
33        self.assertEqual(
34            headers.pop('Server-Software'),
35            header_server,
36            'server software header',
37        )
38
39        date = headers.pop('Date')
40        self.assertEqual(date[-4:], ' GMT', 'date header timezone')
41        self.assertLess(
42            abs(self.date_to_sec_epoch(date) - self.sec_epoch()),
43            5,
44            'date header',
45        )
46
47        self.assertDictEqual(
48            headers,
49            {
50                'Connection': 'close',
51                'Content-Length': str(len(body)),
52                'Content-Type': 'text/html',
53                'Request-Method': 'POST',
54                'Request-Uri': '/',
55                'Http-Host': 'localhost',
56                'Server-Protocol': 'HTTP/1.1',
57                'Custom-Header': 'blah',
58                'Wsgi-Version': '(1, 0)',
59                'Wsgi-Url-Scheme': 'http',
60                'Wsgi-Multithread': 'False',
61                'Wsgi-Multiprocess': 'True',
62                'Wsgi-Run-Once': 'False',
63            },
64            'headers',
65        )
66        self.assertEqual(resp['body'], body, 'body')
67
68    def test_python_application_query_string(self):
69        self.load('query_string')
70
71        resp = self.get(url='/?var1=val1&var2=val2')
72
73        self.assertEqual(
74            resp['headers']['Query-String'],
75            'var1=val1&var2=val2',
76            'Query-String header',
77        )
78
79    def test_python_application_query_string_space(self):
80        self.load('query_string')
81
82        resp = self.get(url='/ ?var1=val1&var2=val2')
83        self.assertEqual(
84            resp['headers']['Query-String'],
85            'var1=val1&var2=val2',
86            'Query-String space',
87        )
88
89        resp = self.get(url='/ %20?var1=val1&var2=val2')
90        self.assertEqual(
91            resp['headers']['Query-String'],
92            'var1=val1&var2=val2',
93            'Query-String space 2',
94        )
95
96        resp = self.get(url='/ %20 ?var1=val1&var2=val2')
97        self.assertEqual(
98            resp['headers']['Query-String'],
99            'var1=val1&var2=val2',
100            'Query-String space 3',
101        )
102
103        resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
104        self.assertEqual(
105            resp['headers']['Query-String'],
106            ' var1= val1 & var2=val2',
107            'Query-String space 4',
108        )
109
110    def test_python_application_query_string_empty(self):
111        self.load('query_string')
112
113        resp = self.get(url='/?')
114
115        self.assertEqual(resp['status'], 200, 'query string empty status')
116        self.assertEqual(
117            resp['headers']['Query-String'], '', 'query string empty'
118        )
119
120    def test_python_application_query_string_absent(self):
121        self.load('query_string')
122
123        resp = self.get()
124
125        self.assertEqual(resp['status'], 200, 'query string absent status')
126        self.assertEqual(
127            resp['headers']['Query-String'], '', 'query string absent'
128        )
129
130    @unittest.skip('not yet')
131    def test_python_application_server_port(self):
132        self.load('server_port')
133
134        self.assertEqual(
135            self.get()['headers']['Server-Port'], '7080', 'Server-Port header'
136        )
137
138    @unittest.skip('not yet')
139    def test_python_application_working_directory_invalid(self):
140        self.load('empty')
141
142        self.assertIn(
143            'success',
144            self.conf('"/blah"', 'applications/empty/working_directory'),
145            'configure invalid working_directory',
146        )
147
148        self.assertEqual(self.get()['status'], 500, 'status')
149
150    def test_python_application_204_transfer_encoding(self):
151        self.load('204_no_content')
152
153        self.assertNotIn(
154            'Transfer-Encoding',
155            self.get()['headers'],
156            '204 header transfer encoding',
157        )
158
159    def test_python_application_ctx_iter_atexit(self):
160        self.load('ctx_iter_atexit')
161
162        resp = self.post(
163            headers={
164                'Host': 'localhost',
165                'Connection': 'close',
166                'Content-Type': 'text/html',
167            },
168            body='0123456789',
169        )
170
171        self.assertEqual(resp['status'], 200, 'ctx iter status')
172        self.assertEqual(resp['body'], '0123456789', 'ctx iter body')
173
174        self.conf({"listeners": {}, "applications": {}})
175
176        self.stop()
177
178        self.assertIsNotNone(
179            self.wait_for_record(r'RuntimeError'), 'ctx iter atexit'
180        )
181
182    def test_python_keepalive_body(self):
183        self.load('mirror')
184
185        self.assertEqual(self.get()['status'], 200, 'init')
186
187        (resp, sock) = self.post(
188            headers={
189                'Host': 'localhost',
190                'Connection': 'keep-alive',
191                'Content-Type': 'text/html',
192            },
193            start=True,
194            body='0123456789' * 500,
195            read_timeout=1,
196        )
197
198        self.assertEqual(resp['body'], '0123456789' * 500, 'keep-alive 1')
199
200        resp = self.post(
201            headers={
202                'Host': 'localhost',
203                'Connection': 'close',
204                'Content-Type': 'text/html',
205            },
206            sock=sock,
207            body='0123456789',
208        )
209
210        self.assertEqual(resp['body'], '0123456789', 'keep-alive 2')
211
212    def test_python_keepalive_reconfigure(self):
213        self.skip_alerts.extend(
214            [
215                r'pthread_mutex.+failed',
216                r'failed to apply',
217                r'process \d+ exited on signal',
218            ]
219        )
220        self.load('mirror')
221
222        self.assertEqual(self.get()['status'], 200, 'init')
223
224        body = '0123456789'
225        conns = 3
226        socks = []
227
228        for i in range(conns):
229            (resp, sock) = self.post(
230                headers={
231                    'Host': 'localhost',
232                    'Connection': 'keep-alive',
233                    'Content-Type': 'text/html',
234                },
235                start=True,
236                body=body,
237                read_timeout=1,
238            )
239
240            self.assertEqual(resp['body'], body, 'keep-alive open')
241            self.assertIn(
242                'success',
243                self.conf(str(i + 1), 'applications/mirror/processes'),
244                'reconfigure',
245            )
246
247            socks.append(sock)
248
249        for i in range(conns):
250            (resp, sock) = self.post(
251                headers={
252                    'Host': 'localhost',
253                    'Connection': 'keep-alive',
254                    'Content-Type': 'text/html',
255                },
256                start=True,
257                sock=socks[i],
258                body=body,
259                read_timeout=1,
260            )
261
262            self.assertEqual(resp['body'], body, 'keep-alive request')
263            self.assertIn(
264                'success',
265                self.conf(str(i + 1), 'applications/mirror/processes'),
266                'reconfigure 2',
267            )
268
269        for i in range(conns):
270            resp = self.post(
271                headers={
272                    'Host': 'localhost',
273                    'Connection': 'close',
274                    'Content-Type': 'text/html',
275                },
276                sock=socks[i],
277                body=body,
278            )
279
280            self.assertEqual(resp['body'], body, 'keep-alive close')
281            self.assertIn(
282                'success',
283                self.conf(str(i + 1), 'applications/mirror/processes'),
284                'reconfigure 3',
285            )
286
287    def test_python_keepalive_reconfigure_2(self):
288        self.load('mirror')
289
290        self.assertEqual(self.get()['status'], 200, 'init')
291
292        body = '0123456789'
293
294        (resp, sock) = self.post(
295            headers={
296                'Host': 'localhost',
297                'Connection': 'keep-alive',
298                'Content-Type': 'text/html',
299            },
300            start=True,
301            body=body,
302            read_timeout=1,
303        )
304
305        self.assertEqual(resp['body'], body, 'reconfigure 2 keep-alive 1')
306
307        self.load('empty')
308
309        self.assertEqual(self.get()['status'], 200, 'init')
310
311        (resp, sock) = self.post(
312            headers={
313                'Host': 'localhost',
314                'Connection': 'close',
315                'Content-Type': 'text/html',
316            },
317            start=True,
318            sock=sock,
319            body=body,
320        )
321
322        self.assertEqual(resp['status'], 200, 'reconfigure 2 keep-alive 2')
323        self.assertEqual(resp['body'], '', 'reconfigure 2 keep-alive 2 body')
324
325        self.assertIn(
326            'success',
327            self.conf({"listeners": {}, "applications": {}}),
328            'reconfigure 2 clear configuration',
329        )
330
331        resp = self.get(sock=sock)
332
333        self.assertEqual(resp, {}, 'reconfigure 2 keep-alive 3')
334
335    def test_python_keepalive_reconfigure_3(self):
336        self.load('empty')
337
338        self.assertEqual(self.get()['status'], 200, 'init')
339
340        (resp, sock) = self.http(
341            b"""GET / HTTP/1.1
342""",
343            start=True,
344            raw=True,
345            read_timeout=5,
346        )
347
348        self.assertIn(
349            'success',
350            self.conf({"listeners": {}, "applications": {}}),
351            'reconfigure 3 clear configuration',
352        )
353
354        resp = self.http(
355            b"""Host: localhost
356Connection: close
357
358""",
359            sock=sock,
360            raw=True,
361        )
362
363        self.assertEqual(resp['status'], 200, 'reconfigure 3')
364
365    def test_python_atexit(self):
366        self.load('atexit')
367
368        self.get()
369
370        self.conf({"listeners": {}, "applications": {}})
371
372        self.stop()
373
374        self.assertIsNotNone(
375            self.wait_for_record(r'At exit called\.'), 'atexit'
376        )
377
378    @unittest.skip('not yet')
379    def test_python_application_start_response_exit(self):
380        self.load('start_response_exit')
381
382        self.assertEqual(self.get()['status'], 500, 'start response exit')
383
384    @unittest.skip('not yet')
385    def test_python_application_input_iter(self):
386        self.load('input_iter')
387
388        body = '0123456789'
389
390        self.assertEqual(self.post(body=body)['body'], body, 'input iter')
391
392    def test_python_application_input_read_length(self):
393        self.load('input_read_length')
394
395        body = '0123456789'
396
397        resp = self.post(
398            headers={
399                'Host': 'localhost',
400                'Input-Length': '5',
401                'Connection': 'close',
402            },
403            body=body,
404        )
405
406        self.assertEqual(resp['body'], body[:5], 'input read length lt body')
407
408        resp = self.post(
409            headers={
410                'Host': 'localhost',
411                'Input-Length': '15',
412                'Connection': 'close',
413            },
414            body=body,
415        )
416
417        self.assertEqual(resp['body'], body, 'input read length gt body')
418
419        resp = self.post(
420            headers={
421                'Host': 'localhost',
422                'Input-Length': '0',
423                'Connection': 'close',
424            },
425            body=body,
426        )
427
428        self.assertEqual(resp['body'], '', 'input read length zero')
429
430        resp = self.post(
431            headers={
432                'Host': 'localhost',
433                'Input-Length': '-1',
434                'Connection': 'close',
435            },
436            body=body,
437        )
438
439        self.assertEqual(resp['body'], body, 'input read length negative')
440
441    @unittest.skip('not yet')
442    def test_python_application_errors_write(self):
443        self.load('errors_write')
444
445        self.get()
446
447        self.stop()
448
449        self.assertIsNotNone(
450            self.wait_for_record(r'\[error\].+Error in application\.'),
451            'errors write',
452        )
453
454    def test_python_application_body_array(self):
455        self.load('body_array')
456
457        self.assertEqual(self.get()['body'], '0123456789', 'body array')
458
459    def test_python_application_body_io(self):
460        self.load('body_io')
461
462        self.assertEqual(self.get()['body'], '0123456789', 'body io')
463
464    def test_python_application_body_io_file(self):
465        self.load('body_io_file')
466
467        self.assertEqual(self.get()['body'], 'body\n', 'body io file')
468
469    @unittest.skip('not yet')
470    def test_python_application_syntax_error(self):
471        self.skip_alerts.append(r'Python failed to import module "wsgi"')
472        self.load('syntax_error')
473
474        self.assertEqual(self.get()['status'], 500, 'syntax error')
475
476    def test_python_application_close(self):
477        self.load('close')
478
479        self.get()
480
481        self.stop()
482
483        self.assertIsNotNone(self.wait_for_record(r'Close called\.'), 'close')
484
485    def test_python_application_close_error(self):
486        self.load('close_error')
487
488        self.get()
489
490        self.stop()
491
492        self.assertIsNotNone(
493            self.wait_for_record(r'Close called\.'), 'close error'
494        )
495
496    def test_python_application_not_iterable(self):
497        self.load('not_iterable')
498
499        self.get()
500
501        self.stop()
502
503        self.assertIsNotNone(
504            self.wait_for_record(
505                r'\[error\].+the application returned not an iterable object'
506            ),
507            'not iterable',
508        )
509
510    def test_python_application_write(self):
511        self.load('write')
512
513        self.assertEqual(self.get()['body'], '0123456789', 'write')
514
515    def test_python_application_threading(self):
516        """wait_for_record() timeouts after 5s while every thread works at
517        least 3s.  So without releasing GIL test should fail.
518        """
519
520        self.load('threading')
521
522        for _ in range(10):
523            self.get(no_recv=True)
524
525        self.assertIsNotNone(
526            self.wait_for_record(r'\(5\) Thread: 100'), 'last thread finished'
527        )
528
529    def test_python_application_iter_exception(self):
530        self.load('iter_exception')
531
532        # Default request doesn't lead to the exception.
533
534        resp = self.get(
535            headers={
536                'Host': 'localhost',
537                'X-Skip': '9',
538                'X-Chunked': '1',
539                'Connection': 'close',
540            }
541        )
542        self.assertEqual(resp['status'], 200, 'status')
543        self.assertEqual(resp['body'][-5:], '0\r\n\r\n', 'body')
544
545        # Exception before start_response().
546
547        self.assertEqual(self.get()['status'], 503, 'error')
548
549        self.assertIsNotNone(self.wait_for_record(r'Traceback'), 'traceback')
550        self.assertIsNotNone(
551            self.wait_for_record(r'raise Exception\(\'first exception\'\)'),
552            'first exception raise',
553        )
554        self.assertEqual(
555            len(self.findall(r'Traceback')), 1, 'traceback count 1'
556        )
557
558        # Exception after start_response(), before first write().
559
560        self.assertEqual(
561            self.get(
562                headers={
563                    'Host': 'localhost',
564                    'X-Skip': '1',
565                    'Connection': 'close',
566                }
567            )['status'],
568            503,
569            'error 2',
570        )
571
572        self.assertIsNotNone(
573            self.wait_for_record(r'raise Exception\(\'second exception\'\)'),
574            'exception raise second',
575        )
576        self.assertEqual(
577            len(self.findall(r'Traceback')), 2, 'traceback count 2'
578        )
579
580        # Exception after first write(), before first __next__().
581
582        _, sock = self.get(
583            headers={
584                'Host': 'localhost',
585                'X-Skip': '2',
586                'Connection': 'keep-alive',
587            },
588            start=True,
589        )
590
591        self.assertIsNotNone(
592            self.wait_for_record(r'raise Exception\(\'third exception\'\)'),
593            'exception raise third',
594        )
595        self.assertEqual(
596            len(self.findall(r'Traceback')), 3, 'traceback count 3'
597        )
598
599        self.assertDictEqual(self.get(sock=sock), {}, 'closed connection')
600
601        # Exception after first write(), before first __next__(),
602        # chunked (incomplete body).
603
604        resp = self.get(
605            headers={
606                'Host': 'localhost',
607                'X-Skip': '2',
608                'X-Chunked': '1',
609                'Connection': 'close',
610            }
611        )
612        if 'body' in resp:
613            self.assertNotEqual(
614                resp['body'][-5:], '0\r\n\r\n', 'incomplete body'
615            )
616        self.assertEqual(
617            len(self.findall(r'Traceback')), 4, 'traceback count 4'
618        )
619
620        # Exception in __next__().
621
622        _, sock = self.get(
623            headers={
624                'Host': 'localhost',
625                'X-Skip': '3',
626                'Connection': 'keep-alive',
627            },
628            start=True,
629        )
630
631        self.assertIsNotNone(
632            self.wait_for_record(r'raise Exception\(\'next exception\'\)'),
633            'exception raise next',
634        )
635        self.assertEqual(
636            len(self.findall(r'Traceback')), 5, 'traceback count 5'
637        )
638
639        self.assertDictEqual(self.get(sock=sock), {}, 'closed connection 2')
640
641        # Exception in __next__(), chunked (incomplete body).
642
643        resp = self.get(
644            headers={
645                'Host': 'localhost',
646                'X-Skip': '3',
647                'X-Chunked': '1',
648                'Connection': 'close',
649            }
650        )
651        if 'body' in resp:
652            self.assertNotEqual(
653                resp['body'][-5:], '0\r\n\r\n', 'incomplete body 2'
654            )
655        self.assertEqual(
656            len(self.findall(r'Traceback')), 6, 'traceback count 6'
657        )
658
659        # Exception before start_response() and in close().
660
661        self.assertEqual(
662            self.get(
663                headers={
664                    'Host': 'localhost',
665                    'X-Not-Skip-Close': '1',
666                    'Connection': 'close',
667                }
668            )['status'],
669            503,
670            'error',
671        )
672
673        self.assertIsNotNone(
674            self.wait_for_record(r'raise Exception\(\'close exception\'\)'),
675            'exception raise close',
676        )
677        self.assertEqual(
678            len(self.findall(r'Traceback')), 8, 'traceback count 8'
679        )
680
681if __name__ == '__main__':
682    TestPythonApplication.main()
683