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