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