xref: /unit/test/test_python_application.py (revision 1635:97afbb6c5a15)
1import grp
2import pwd
3import re
4import time
5
6import pytest
7
8from conftest import skip_alert
9from unit.applications.lang.python import TestApplicationPython
10
11
12class TestPythonApplication(TestApplicationPython):
13    prerequisites = {'modules': {'python': 'all'}}
14
15    def findall(self, pattern):
16        with open(self.temp_dir + '/unit.log', 'r', errors='ignore') as f:
17            return re.findall(pattern, f.read())
18
19    def test_python_application_variables(self):
20        self.load('variables')
21
22        body = 'Test body string.'
23
24        resp = self.http(
25            b"""POST / HTTP/1.1
26Host: localhost
27Content-Length: %d
28Custom-Header: blah
29Custom-hEader: Blah
30Content-Type: text/html
31Connection: close
32custom-header: BLAH
33
34%s""" % (len(body), body.encode()),
35            raw=True,
36        )
37
38        assert resp['status'] == 200, 'status'
39        headers = resp['headers']
40        header_server = headers.pop('Server')
41        assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
42        assert (
43            headers.pop('Server-Software') == header_server
44        ), 'server software header'
45
46        date = headers.pop('Date')
47        assert date[-4:] == ' GMT', 'date header timezone'
48        assert (
49            abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5
50        ), 'date header'
51
52        assert headers == {
53            'Connection': 'close',
54            'Content-Length': str(len(body)),
55            'Content-Type': 'text/html',
56            'Request-Method': 'POST',
57            'Request-Uri': '/',
58            'Http-Host': 'localhost',
59            'Server-Protocol': 'HTTP/1.1',
60            'Custom-Header': 'blah, Blah, BLAH',
61            'Wsgi-Version': '(1, 0)',
62            'Wsgi-Url-Scheme': 'http',
63            'Wsgi-Multithread': 'False',
64            'Wsgi-Multiprocess': 'True',
65            'Wsgi-Run-Once': 'False',
66        }, 'headers'
67        assert resp['body'] == body, 'body'
68
69    def test_python_application_query_string(self):
70        self.load('query_string')
71
72        resp = self.get(url='/?var1=val1&var2=val2')
73
74        assert (
75            resp['headers']['Query-String'] == 'var1=val1&var2=val2'
76        ), 'Query-String header'
77
78    def test_python_application_query_string_space(self):
79        self.load('query_string')
80
81        resp = self.get(url='/ ?var1=val1&var2=val2')
82        assert (
83            resp['headers']['Query-String'] == 'var1=val1&var2=val2'
84        ), 'Query-String space'
85
86        resp = self.get(url='/ %20?var1=val1&var2=val2')
87        assert (
88            resp['headers']['Query-String'] == 'var1=val1&var2=val2'
89        ), 'Query-String space 2'
90
91        resp = self.get(url='/ %20 ?var1=val1&var2=val2')
92        assert (
93            resp['headers']['Query-String'] == 'var1=val1&var2=val2'
94        ), 'Query-String space 3'
95
96        resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
97        assert (
98            resp['headers']['Query-String'] == ' var1= val1 & var2=val2'
99        ), 'Query-String space 4'
100
101    def test_python_application_query_string_empty(self):
102        self.load('query_string')
103
104        resp = self.get(url='/?')
105
106        assert resp['status'] == 200, 'query string empty status'
107        assert resp['headers']['Query-String'] == '', 'query string empty'
108
109    def test_python_application_query_string_absent(self):
110        self.load('query_string')
111
112        resp = self.get()
113
114        assert resp['status'] == 200, 'query string absent status'
115        assert resp['headers']['Query-String'] == '', 'query string absent'
116
117    @pytest.mark.skip('not yet')
118    def test_python_application_server_port(self):
119        self.load('server_port')
120
121        assert (
122            self.get()['headers']['Server-Port'] == '7080'
123        ), 'Server-Port header'
124
125    @pytest.mark.skip('not yet')
126    def test_python_application_working_directory_invalid(self):
127        self.load('empty')
128
129        assert 'success' in self.conf(
130            '"/blah"', 'applications/empty/working_directory'
131        ), 'configure invalid working_directory'
132
133        assert self.get()['status'] == 500, 'status'
134
135    def test_python_application_204_transfer_encoding(self):
136        self.load('204_no_content')
137
138        assert (
139            'Transfer-Encoding' not in self.get()['headers']
140        ), '204 header transfer encoding'
141
142    def test_python_application_ctx_iter_atexit(self):
143        self.load('ctx_iter_atexit')
144
145        resp = self.post(
146            headers={
147                'Host': 'localhost',
148                'Connection': 'close',
149                'Content-Type': 'text/html',
150            },
151            body='0123456789',
152        )
153
154        assert resp['status'] == 200, 'ctx iter status'
155        assert resp['body'] == '0123456789', 'ctx iter body'
156
157        self.conf({"listeners": {}, "applications": {}})
158
159        self.stop()
160
161        assert (
162            self.wait_for_record(r'RuntimeError') is not None
163        ), 'ctx iter atexit'
164
165    def test_python_keepalive_body(self):
166        self.load('mirror')
167
168        assert self.get()['status'] == 200, 'init'
169
170        body = '0123456789' * 500
171        (resp, sock) = self.post(
172            headers={
173                'Host': 'localhost',
174                'Connection': 'keep-alive',
175                'Content-Type': 'text/html',
176            },
177            start=True,
178            body=body,
179            read_timeout=1,
180        )
181
182        assert resp['body'] == body, 'keep-alive 1'
183
184        body = '0123456789'
185        resp = self.post(
186            headers={
187                'Host': 'localhost',
188                'Connection': 'close',
189                'Content-Type': 'text/html',
190            },
191            sock=sock,
192            body=body,
193        )
194
195        assert resp['body'] == body, 'keep-alive 2'
196
197    def test_python_keepalive_reconfigure(self):
198        skip_alert(
199            r'pthread_mutex.+failed',
200            r'failed to apply',
201            r'process \d+ exited on signal',
202        )
203        self.load('mirror')
204
205        assert self.get()['status'] == 200, 'init'
206
207        body = '0123456789'
208        conns = 3
209        socks = []
210
211        for i in range(conns):
212            (resp, sock) = self.post(
213                headers={
214                    'Host': 'localhost',
215                    'Connection': 'keep-alive',
216                    'Content-Type': 'text/html',
217                },
218                start=True,
219                body=body,
220                read_timeout=1,
221            )
222
223            assert resp['body'] == body, 'keep-alive open'
224            assert 'success' in self.conf(
225                str(i + 1), 'applications/mirror/processes'
226            ), 'reconfigure'
227
228            socks.append(sock)
229
230        for i in range(conns):
231            (resp, sock) = self.post(
232                headers={
233                    'Host': 'localhost',
234                    'Connection': 'keep-alive',
235                    'Content-Type': 'text/html',
236                },
237                start=True,
238                sock=socks[i],
239                body=body,
240                read_timeout=1,
241            )
242
243            assert resp['body'] == body, 'keep-alive request'
244            assert 'success' in self.conf(
245                str(i + 1), 'applications/mirror/processes'
246            ), 'reconfigure 2'
247
248        for i in range(conns):
249            resp = self.post(
250                headers={
251                    'Host': 'localhost',
252                    'Connection': 'close',
253                    'Content-Type': 'text/html',
254                },
255                sock=socks[i],
256                body=body,
257            )
258
259            assert resp['body'] == body, 'keep-alive close'
260            assert 'success' in self.conf(
261                str(i + 1), 'applications/mirror/processes'
262            ), 'reconfigure 3'
263
264    def test_python_keepalive_reconfigure_2(self):
265        self.load('mirror')
266
267        assert self.get()['status'] == 200, 'init'
268
269        body = '0123456789'
270
271        (resp, sock) = self.post(
272            headers={
273                'Host': 'localhost',
274                'Connection': 'keep-alive',
275                'Content-Type': 'text/html',
276            },
277            start=True,
278            body=body,
279            read_timeout=1,
280        )
281
282        assert resp['body'] == body, 'reconfigure 2 keep-alive 1'
283
284        self.load('empty')
285
286        assert self.get()['status'] == 200, 'init'
287
288        (resp, sock) = self.post(
289            headers={
290                'Host': 'localhost',
291                'Connection': 'close',
292                'Content-Type': 'text/html',
293            },
294            start=True,
295            sock=sock,
296            body=body,
297        )
298
299        assert resp['status'] == 200, 'reconfigure 2 keep-alive 2'
300        assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body'
301
302        assert 'success' in self.conf(
303            {"listeners": {}, "applications": {}}
304        ), 'reconfigure 2 clear configuration'
305
306        resp = self.get(sock=sock)
307
308        assert resp == {}, 'reconfigure 2 keep-alive 3'
309
310    def test_python_keepalive_reconfigure_3(self):
311        self.load('empty')
312
313        assert self.get()['status'] == 200, 'init'
314
315        (_, sock) = self.http(
316            b"""GET / HTTP/1.1
317""",
318            start=True,
319            raw=True,
320            no_recv=True,
321        )
322
323        assert self.get()['status'] == 200
324
325        assert 'success' in self.conf(
326            {"listeners": {}, "applications": {}}
327        ), 'reconfigure 3 clear configuration'
328
329        resp = self.http(
330            b"""Host: localhost
331Connection: close
332
333""",
334            sock=sock,
335            raw=True,
336        )
337
338        assert resp['status'] == 200, 'reconfigure 3'
339
340    def test_python_atexit(self):
341        self.load('atexit')
342
343        self.get()
344
345        self.conf({"listeners": {}, "applications": {}})
346
347        self.stop()
348
349        assert self.wait_for_record(r'At exit called\.') is not None, 'atexit'
350
351    def test_python_process_switch(self):
352        self.load('delayed')
353
354        assert 'success' in self.conf(
355            '2', 'applications/delayed/processes'
356        ), 'configure 2 processes'
357
358        self.get(
359            headers={
360                'Host': 'localhost',
361                'Content-Length': '0',
362                'X-Delay': '5',
363                'Connection': 'close',
364            },
365            no_recv=True,
366        )
367
368        headers_delay_1 = {
369            'Connection': 'close',
370            'Host': 'localhost',
371            'Content-Length': '0',
372            'X-Delay': '1',
373        }
374
375        self.get(headers=headers_delay_1, no_recv=True)
376
377        time.sleep(0.5)
378
379        for _ in range(10):
380            self.get(headers=headers_delay_1, no_recv=True)
381
382        self.get(headers=headers_delay_1)
383
384    @pytest.mark.skip('not yet')
385    def test_python_application_start_response_exit(self):
386        self.load('start_response_exit')
387
388        assert self.get()['status'] == 500, 'start response exit'
389
390    def test_python_application_input_iter(self):
391        self.load('input_iter')
392
393        body = '''0123456789
394next line
395
396last line'''
397
398        resp = self.post(body=body)
399        assert resp['body'] == body, 'input iter'
400        assert resp['headers']['X-Lines-Count'] == '4', 'input iter lines'
401
402    def test_python_application_input_readline(self):
403        self.load('input_readline')
404
405        body = '''0123456789
406next line
407
408last line'''
409
410        resp = self.post(body=body)
411        assert resp['body'] == body, 'input readline'
412        assert resp['headers']['X-Lines-Count'] == '4', 'input readline lines'
413
414    def test_python_application_input_readline_size(self):
415        self.load('input_readline_size')
416
417        body = '''0123456789
418next line
419
420last line'''
421
422        assert self.post(body=body)['body'] == body, 'input readline size'
423        assert (
424            self.post(body='0123')['body'] == '0123'
425        ), 'input readline size less'
426
427    def test_python_application_input_readlines(self):
428        self.load('input_readlines')
429
430        body = '''0123456789
431next line
432
433last line'''
434
435        resp = self.post(body=body)
436        assert resp['body'] == body, 'input readlines'
437        assert resp['headers']['X-Lines-Count'] == '4', 'input readlines lines'
438
439    def test_python_application_input_readlines_huge(self):
440        self.load('input_readlines')
441
442        body = (
443            '''0123456789 abcdefghi
444next line: 0123456789 abcdefghi
445
446last line: 987654321
447'''
448            * 512
449        )
450
451        assert (
452            self.post(body=body, read_buffer_size=16384)['body'] == body
453        ), 'input readlines huge'
454
455    def test_python_application_input_read_length(self):
456        self.load('input_read_length')
457
458        body = '0123456789'
459
460        resp = self.post(
461            headers={
462                'Host': 'localhost',
463                'Input-Length': '5',
464                'Connection': 'close',
465            },
466            body=body,
467        )
468
469        assert resp['body'] == body[:5], 'input read length lt body'
470
471        resp = self.post(
472            headers={
473                'Host': 'localhost',
474                'Input-Length': '15',
475                'Connection': 'close',
476            },
477            body=body,
478        )
479
480        assert resp['body'] == body, 'input read length gt body'
481
482        resp = self.post(
483            headers={
484                'Host': 'localhost',
485                'Input-Length': '0',
486                'Connection': 'close',
487            },
488            body=body,
489        )
490
491        assert resp['body'] == '', 'input read length zero'
492
493        resp = self.post(
494            headers={
495                'Host': 'localhost',
496                'Input-Length': '-1',
497                'Connection': 'close',
498            },
499            body=body,
500        )
501
502        assert resp['body'] == body, 'input read length negative'
503
504    @pytest.mark.skip('not yet')
505    def test_python_application_errors_write(self):
506        self.load('errors_write')
507
508        self.get()
509
510        self.stop()
511
512        assert (
513            self.wait_for_record(r'\[error\].+Error in application\.')
514            is not None
515        ), 'errors write'
516
517    def test_python_application_body_array(self):
518        self.load('body_array')
519
520        assert self.get()['body'] == '0123456789', 'body array'
521
522    def test_python_application_body_io(self):
523        self.load('body_io')
524
525        assert self.get()['body'] == '0123456789', 'body io'
526
527    def test_python_application_body_io_file(self):
528        self.load('body_io_file')
529
530        assert self.get()['body'] == 'body\n', 'body io file'
531
532    @pytest.mark.skip('not yet')
533    def test_python_application_syntax_error(self):
534        skip_alert(r'Python failed to import module "wsgi"')
535        self.load('syntax_error')
536
537        assert self.get()['status'] == 500, 'syntax error'
538
539    def test_python_application_loading_error(self):
540        skip_alert(r'Python failed to import module "blah"')
541
542        self.load('empty')
543
544        assert 'success' in self.conf('"blah"', 'applications/empty/module')
545
546        assert self.get()['status'] == 503, 'loading error'
547
548    def test_python_application_close(self):
549        self.load('close')
550
551        self.get()
552
553        self.stop()
554
555        assert self.wait_for_record(r'Close called\.') is not None, 'close'
556
557    def test_python_application_close_error(self):
558        self.load('close_error')
559
560        self.get()
561
562        self.stop()
563
564        assert (
565            self.wait_for_record(r'Close called\.') is not None
566        ), 'close error'
567
568    def test_python_application_not_iterable(self):
569        self.load('not_iterable')
570
571        self.get()
572
573        self.stop()
574
575        assert (
576            self.wait_for_record(
577                r'\[error\].+the application returned not an iterable object'
578            )
579            is not None
580        ), 'not iterable'
581
582    def test_python_application_write(self):
583        self.load('write')
584
585        assert self.get()['body'] == '0123456789', 'write'
586
587    def test_python_application_threading(self):
588        """wait_for_record() timeouts after 5s while every thread works at
589        least 3s.  So without releasing GIL test should fail.
590        """
591
592        self.load('threading')
593
594        for _ in range(10):
595            self.get(no_recv=True)
596
597        assert (
598            self.wait_for_record(r'\(5\) Thread: 100') is not None
599        ), 'last thread finished'
600
601    def test_python_application_iter_exception(self):
602        self.load('iter_exception')
603
604        # Default request doesn't lead to the exception.
605
606        resp = self.get(
607            headers={
608                'Host': 'localhost',
609                'X-Skip': '9',
610                'X-Chunked': '1',
611                'Connection': 'close',
612            }
613        )
614        assert resp['status'] == 200, 'status'
615        assert resp['body'] == 'XXXXXXX', 'body'
616
617        # Exception before start_response().
618
619        assert self.get()['status'] == 503, 'error'
620
621        assert self.wait_for_record(r'Traceback') is not None, 'traceback'
622        assert (
623            self.wait_for_record(r'raise Exception\(\'first exception\'\)')
624            is not None
625        ), 'first exception raise'
626        assert len(self.findall(r'Traceback')) == 1, 'traceback count 1'
627
628        # Exception after start_response(), before first write().
629
630        assert (
631            self.get(
632                headers={
633                    'Host': 'localhost',
634                    'X-Skip': '1',
635                    'Connection': 'close',
636                }
637            )['status']
638            == 503
639        ), 'error 2'
640
641        assert (
642            self.wait_for_record(r'raise Exception\(\'second exception\'\)')
643            is not None
644        ), 'exception raise second'
645        assert len(self.findall(r'Traceback')) == 2, 'traceback count 2'
646
647        # Exception after first write(), before first __next__().
648
649        _, sock = self.get(
650            headers={
651                'Host': 'localhost',
652                'X-Skip': '2',
653                'Connection': 'keep-alive',
654            },
655            start=True,
656        )
657
658        assert (
659            self.wait_for_record(r'raise Exception\(\'third exception\'\)')
660            is not None
661        ), 'exception raise third'
662        assert len(self.findall(r'Traceback')) == 3, 'traceback count 3'
663
664        assert self.get(sock=sock) == {}, 'closed connection'
665
666        # Exception after first write(), before first __next__(),
667        # chunked (incomplete body).
668
669        resp = self.get(
670            headers={
671                'Host': 'localhost',
672                'X-Skip': '2',
673                'X-Chunked': '1',
674                'Connection': 'close',
675            },
676            raw_resp=True,
677        )
678        if resp:
679            assert resp[-5:] != '0\r\n\r\n', 'incomplete body'
680        assert len(self.findall(r'Traceback')) == 4, 'traceback count 4'
681
682        # Exception in __next__().
683
684        _, sock = self.get(
685            headers={
686                'Host': 'localhost',
687                'X-Skip': '3',
688                'Connection': 'keep-alive',
689            },
690            start=True,
691        )
692
693        assert (
694            self.wait_for_record(r'raise Exception\(\'next exception\'\)')
695            is not None
696        ), 'exception raise next'
697        assert len(self.findall(r'Traceback')) == 5, 'traceback count 5'
698
699        assert self.get(sock=sock) == {}, 'closed connection 2'
700
701        # Exception in __next__(), chunked (incomplete body).
702
703        resp = self.get(
704            headers={
705                'Host': 'localhost',
706                'X-Skip': '3',
707                'X-Chunked': '1',
708                'Connection': 'close',
709            },
710            raw_resp=True,
711        )
712        if resp:
713            assert resp[-5:] != '0\r\n\r\n', 'incomplete body 2'
714        assert len(self.findall(r'Traceback')) == 6, 'traceback count 6'
715
716        # Exception before start_response() and in close().
717
718        assert (
719            self.get(
720                headers={
721                    'Host': 'localhost',
722                    'X-Not-Skip-Close': '1',
723                    'Connection': 'close',
724                }
725            )['status']
726            == 503
727        ), 'error'
728
729        assert (
730            self.wait_for_record(r'raise Exception\(\'close exception\'\)')
731            is not None
732        ), 'exception raise close'
733        assert len(self.findall(r'Traceback')) == 8, 'traceback count 8'
734
735    def test_python_user_group(self, is_su):
736        if not is_su:
737            pytest.skip('requires root')
738
739        nobody_uid = pwd.getpwnam('nobody').pw_uid
740
741        group = 'nobody'
742
743        try:
744            group_id = grp.getgrnam(group).gr_gid
745        except:
746            group = 'nogroup'
747            group_id = grp.getgrnam(group).gr_gid
748
749        self.load('user_group')
750
751        obj = self.getjson()['body']
752        assert obj['UID'] == nobody_uid, 'nobody uid'
753        assert obj['GID'] == group_id, 'nobody gid'
754
755        self.load('user_group', user='nobody')
756
757        obj = self.getjson()['body']
758        assert obj['UID'] == nobody_uid, 'nobody uid user=nobody'
759        assert obj['GID'] == group_id, 'nobody gid user=nobody'
760
761        self.load('user_group', user='nobody', group=group)
762
763        obj = self.getjson()['body']
764        assert obj['UID'] == nobody_uid, (
765            'nobody uid user=nobody group=%s' % group
766        )
767
768        assert obj['GID'] == group_id, (
769            'nobody gid user=nobody group=%s' % group
770        )
771
772        self.load('user_group', group=group)
773
774        obj = self.getjson()['body']
775        assert obj['UID'] == nobody_uid, 'nobody uid group=%s' % group
776
777        assert obj['GID'] == group_id, 'nobody gid group=%s' % group
778
779        self.load('user_group', user='root')
780
781        obj = self.getjson()['body']
782        assert obj['UID'] == 0, 'root uid user=root'
783        assert obj['GID'] == 0, 'root gid user=root'
784
785        group = 'root'
786
787        try:
788            grp.getgrnam(group)
789            group = True
790        except:
791            group = False
792
793        if group:
794            self.load('user_group', user='root', group='root')
795
796            obj = self.getjson()['body']
797            assert obj['UID'] == 0, 'root uid user=root group=root'
798            assert obj['GID'] == 0, 'root gid user=root group=root'
799
800            self.load('user_group', group='root')
801
802            obj = self.getjson()['body']
803            assert obj['UID'] == nobody_uid, 'root uid group=root'
804            assert obj['GID'] == 0, 'root gid group=root'
805
806    def test_python_application_callable(self):
807        skip_alert(r'Python failed to get "blah" from module')
808        self.load('callable')
809
810        assert self.get()['status'] == 204, 'default application response'
811
812        assert 'success' in self.conf(
813            '"app"', 'applications/callable/callable'
814        )
815
816        assert self.get()['status'] == 200, 'callable response'
817
818        assert 'success' in self.conf(
819            '"blah"', 'applications/callable/callable'
820        )
821
822        assert self.get()['status'] not in [200, 204], 'callable response inv'
823
824        assert 'success' in self.conf(
825            '"app"', 'applications/callable/callable'
826        )
827
828        assert self.get()['status'] == 200, 'callable response 2'
829
830        assert 'success' in self.conf_delete('applications/callable/callable')
831
832        assert self.get()['status'] == 204, 'default response 2'
833