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