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