OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Swarming Authors. All rights reserved. | 2 # Copyright 2014 The Swarming Authors. All rights reserved. |
3 # Use of this source code is governed by the Apache v2.0 license that can be | 3 # Use of this source code is governed by the Apache v2.0 license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Integration test for the Swarming server, Swarming bot and Swarming client. | 6 """Integration test for the Swarming server, Swarming bot and Swarming client. |
7 | 7 |
8 It starts both a Swarming server and a Swarming bot and triggers tasks with the | 8 It starts both a Swarming server and a Swarming bot and triggers tasks with the |
9 Swarming client to ensure the system works end to end. | 9 Swarming client to ensure the system works end to end. |
10 """ | 10 """ |
(...skipping 25 matching lines...) Expand all Loading... |
36 import test_env_bot | 36 import test_env_bot |
37 test_env_bot.init_symlinks(BOT_DIR) | 37 test_env_bot.init_symlinks(BOT_DIR) |
38 | 38 |
39 from api import os_utilities | 39 from api import os_utilities |
40 | 40 |
41 | 41 |
42 # Signal as seen by the process. | 42 # Signal as seen by the process. |
43 SIGNAL_TERM = -1073741510 if sys.platform == 'win32' else -signal.SIGTERM | 43 SIGNAL_TERM = -1073741510 if sys.platform == 'win32' else -signal.SIGTERM |
44 | 44 |
45 | 45 |
| 46 # For the isolated tests that outputs a file named result.txt containing 'hey'. |
| 47 ISOLATE_HELLO_WORLD = { |
| 48 'variables': { |
| 49 'command': ['python', '-u', 'hello_world.py'], |
| 50 'files': ['hello_world.py'], |
| 51 }, |
| 52 } |
| 53 |
| 54 RESULT_HEY_ISOLATED_OUT = { |
| 55 u'isolated': u'f10f4c42b38ca01726610f9575ba695468c32108', |
| 56 u'isolatedserver': u'http://localhost:10050', |
| 57 u'namespace': u'default-gzip', |
| 58 u'view_url': |
| 59 u'http://localhost:10050/browse?namespace=default-gzip' |
| 60 '&hash=f10f4c42b38ca01726610f9575ba695468c32108', |
| 61 } |
| 62 |
| 63 RESULT_HEY_OUTPUTS_REF = { |
| 64 u'isolated': u'f10f4c42b38ca01726610f9575ba695468c32108', |
| 65 u'isolatedserver': u'http://localhost:10050', |
| 66 u'namespace': u'default-gzip', |
| 67 u'view_url': |
| 68 u'http://localhost:10050/browse?namespace=default-gzip' |
| 69 '&hash=f10f4c42b38ca01726610f9575ba695468c32108', |
| 70 } |
| 71 |
| 72 |
46 class SwarmingClient(object): | 73 class SwarmingClient(object): |
47 def __init__(self, swarming_server, isolate_server): | 74 def __init__(self, swarming_server, isolate_server): |
48 self._swarming_server = swarming_server | 75 self._swarming_server = swarming_server |
49 self._isolate_server = isolate_server | 76 self._isolate_server = isolate_server |
50 self._tmpdir = tempfile.mkdtemp(prefix='swarming_client') | 77 self._tmpdir = tempfile.mkdtemp(prefix='swarming_client') |
51 self._index = 0 | 78 self._index = 0 |
52 | 79 |
53 def isolate(self, isolate_path, isolated_path): | 80 def isolate(self, isolate_path, isolated_path): |
54 cmd = [ | 81 cmd = [ |
55 sys.executable, 'isolate.py', 'archive', | 82 sys.executable, 'isolate.py', 'archive', |
(...skipping 274 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
330 # account. | 357 # account. |
331 res = self.servers.http_client.request( | 358 res = self.servers.http_client.request( |
332 '/restricted/upload/bot_config', | 359 '/restricted/upload/bot_config', |
333 body=urllib.urlencode({'script': bot_config_content})) | 360 body=urllib.urlencode({'script': bot_config_content})) |
334 self.assertEqual(200, res.http_code, res.body) | 361 self.assertEqual(200, res.http_code, res.body) |
335 bot_version2 = self.assertOneTask(args, summary, {}) | 362 bot_version2 = self.assertOneTask(args, summary, {}) |
336 self.assertNotEqual(bot_version1, bot_version2) | 363 self.assertNotEqual(bot_version1, bot_version2) |
337 | 364 |
338 def test_isolated(self): | 365 def test_isolated(self): |
339 # Make an isolated file, archive it. | 366 # Make an isolated file, archive it. |
340 isolate = { | |
341 'variables': { | |
342 'command': ['python', 'hello_world.py'], | |
343 'files': ['hello_world.py'], | |
344 }, | |
345 } | |
346 hello_world = '\n'.join(( | 367 hello_world = '\n'.join(( |
347 'import os', | 368 'import os', |
348 'import sys', | 369 'import sys', |
349 'print(\'hi\')', | 370 'print(\'hi\')', |
350 'with open(os.path.join(sys.argv[1], \'result.txt\'), \'wb\') as f:', | 371 'with open(os.path.join(sys.argv[1], \'result.txt\'), \'wb\') as f:', |
351 ' f.write(\'hey\')')) | 372 ' f.write(\'hey\')')) |
352 expected_summary = self.gen_expected( | 373 expected_summary = self.gen_expected( |
353 name=u'yo', | 374 name=u'isolated_task', |
354 isolated_out={ | 375 isolated_out=RESULT_HEY_ISOLATED_OUT, |
355 u'isolated': u'f10f4c42b38ca01726610f9575ba695468c32108', | 376 outputs=[u'hi\n'], |
356 u'isolatedserver': u'http://localhost:10050', | 377 outputs_ref=RESULT_HEY_OUTPUTS_REF) |
357 u'namespace': u'default-gzip', | |
358 u'view_url': | |
359 u'http://localhost:10050/browse?namespace=default-gzip' | |
360 '&hash=f10f4c42b38ca01726610f9575ba695468c32108', | |
361 }, | |
362 outputs=[ | |
363 u'hi\n' | |
364 ], | |
365 outputs_ref={ | |
366 u'isolated': u'f10f4c42b38ca01726610f9575ba695468c32108', | |
367 u'isolatedserver': u'http://localhost:10050', | |
368 u'namespace': u'default-gzip', | |
369 u'view_url': | |
370 u'http://localhost:10050/browse?namespace=default-gzip' | |
371 '&hash=f10f4c42b38ca01726610f9575ba695468c32108', | |
372 }) | |
373 expected_files = {os.path.join('0', 'result.txt'): 'hey'} | 378 expected_files = {os.path.join('0', 'result.txt'): 'hey'} |
| 379 self._run_isolated( |
| 380 hello_world, 'isolated_task', ['--', '${ISOLATED_OUTDIR}'], |
| 381 expected_summary, expected_files) |
| 382 |
| 383 def test_isolated_hard_timeout(self): |
| 384 # Make an isolated file, archive it, have it time out. Similar to |
| 385 # test_hard_timeout. The script doesn't handle signal so it failed the grace |
| 386 # period. |
| 387 hello_world = '\n'.join(( |
| 388 'import os', |
| 389 'import sys', |
| 390 'import time', |
| 391 'sys.stdout.write(\'hi\\n\')', |
| 392 'sys.stdout.flush()', |
| 393 'time.sleep(120)', |
| 394 'with open(os.path.join(sys.argv[1], \'result.txt\'), \'wb\') as f:', |
| 395 ' f.write(\'hey\')')) |
| 396 expected_summary = self.gen_expected( |
| 397 name=u'isolated_hard_timeout', |
| 398 exit_codes=[SIGNAL_TERM], |
| 399 failure=True, |
| 400 state=0x40) # task_result.State.TIMED_OUT |
| 401 self._run_isolated( |
| 402 hello_world, 'isolated_hard_timeout', |
| 403 ['--hard-timeout', '1', '--', '${ISOLATED_OUTDIR}'], |
| 404 expected_summary, {}) |
| 405 |
| 406 def test_isolated_hard_timeout_grace(self): |
| 407 # Make an isolated file, archive it, have it time out. Similar to |
| 408 # test_hard_timeout. The script handles signal so it send results back. |
| 409 hello_world = '\n'.join(( |
| 410 'import os', |
| 411 'import signal', |
| 412 'import sys', |
| 413 'import time', |
| 414 'l = []', |
| 415 'def handler(signum, _):', |
| 416 ' l.append(signum)', |
| 417 ' sys.stdout.write(\'got signal %d\\n\' % signum)', |
| 418 ' sys.stdout.flush()', |
| 419 'signal.signal(signal.%s, handler)' % |
| 420 ('SIGBREAK' if sys.platform == 'win32' else 'SIGTERM'), |
| 421 'sys.stdout.write(\'hi\\n\')', |
| 422 'sys.stdout.flush()', |
| 423 'while not l:', |
| 424 ' try:', |
| 425 ' time.sleep(0.01)', |
| 426 ' except IOError:', |
| 427 ' print(\'ioerror\')', |
| 428 'with open(os.path.join(sys.argv[1], \'result.txt\'), \'wb\') as f:', |
| 429 ' f.write(\'hey\')')) |
| 430 expected_summary = self.gen_expected( |
| 431 name=u'isolated_hard_timeout_grace', |
| 432 isolated_out=RESULT_HEY_ISOLATED_OUT, |
| 433 outputs=[u'hi\ngot signal 15\n'], |
| 434 outputs_ref=RESULT_HEY_OUTPUTS_REF, |
| 435 failure=True, |
| 436 state=0x40) # task_result.State.TIMED_OUT |
| 437 expected_files = {os.path.join('0', 'result.txt'): 'hey'} |
| 438 # Sadly we have to use a slow timeout here as this test can be flaky; it |
| 439 # will not result in the expected result if run_isolated receives the signal |
| 440 # before it had time to download the files, map them and start the child |
| 441 # process, which then had time to setup its handler. |
| 442 # TODO(maruel): When using run_isolated, have run_isolated enforces the hard |
| 443 # timeout, while I/O timeout is still enforced by task_runner. This is due |
| 444 # to run_isolated not piping stdout so it doesn't know about stdout/stderr |
| 445 # output. Once this is fixed, the timeout can be reduced back to 1s. |
| 446 self._run_isolated( |
| 447 hello_world, 'isolated_hard_timeout_grace', |
| 448 ['--hard-timeout', '3', '--', '${ISOLATED_OUTDIR}'], |
| 449 expected_summary, expected_files) |
| 450 |
| 451 def _run_isolated(self, hello_world, name, args, expected_summary, |
| 452 expected_files): |
| 453 # Shared code for all test_isolated_* test cases. |
374 tmpdir = tempfile.mkdtemp(prefix='swarming_smoke') | 454 tmpdir = tempfile.mkdtemp(prefix='swarming_smoke') |
375 try: | 455 try: |
376 isolate_path = os.path.join(tmpdir, 'i.isolate') | 456 isolate_path = os.path.join(tmpdir, 'i.isolate') |
377 isolated_path = os.path.join(tmpdir, 'i.isolated') | 457 isolated_path = os.path.join(tmpdir, 'i.isolated') |
378 with open(isolate_path, 'wb') as f: | 458 with open(isolate_path, 'wb') as f: |
379 json.dump(isolate, f) | 459 json.dump(ISOLATE_HELLO_WORLD, f) |
380 with open(os.path.join(tmpdir, 'hello_world.py'), 'wb') as f: | 460 with open(os.path.join(tmpdir, 'hello_world.py'), 'wb') as f: |
381 f.write(hello_world) | 461 f.write(hello_world) |
382 isolated_hash = self.client.isolate(isolate_path, isolated_path) | 462 isolated_hash = self.client.isolate(isolate_path, isolated_path) |
383 task_id = self.client.task_trigger_isolated( | 463 task_id = self.client.task_trigger_isolated( |
384 'yo', isolated_hash, extra=['--', '${ISOLATED_OUTDIR}']) | 464 name, isolated_hash, extra=args) |
385 actual_summary, actual_files = self.client.task_collect(task_id) | 465 actual_summary, actual_files = self.client.task_collect(task_id) |
386 self.assertResults(expected_summary, actual_summary) | 466 self.assertResults(expected_summary, actual_summary) |
387 actual_files.pop('summary.json') | 467 actual_files.pop('summary.json') |
388 self.assertEqual(expected_files, actual_files) | 468 self.assertEqual(expected_files, actual_files) |
389 finally: | 469 finally: |
390 shutil.rmtree(tmpdir) | 470 shutil.rmtree(tmpdir) |
391 | 471 |
392 def assertResults(self, expected, result): | 472 def assertResults(self, expected, result): |
393 self.assertEqual(['shards'], result.keys()) | 473 self.assertEqual(['shards'], result.keys()) |
394 self.assertEqual(1, len(result['shards'])) | 474 self.assertEqual(1, len(result['shards'])) |
(...skipping 15 matching lines...) Expand all Loading... |
410 def assertOneTask(self, args, expected_summary, expected_files): | 490 def assertOneTask(self, args, expected_summary, expected_files): |
411 """Runs a single task at a time.""" | 491 """Runs a single task at a time.""" |
412 task_id = self.client.task_trigger_raw(args) | 492 task_id = self.client.task_trigger_raw(args) |
413 actual_summary, actual_files = self.client.task_collect(task_id) | 493 actual_summary, actual_files = self.client.task_collect(task_id) |
414 bot_version = self.assertResults(expected_summary, actual_summary) | 494 bot_version = self.assertResults(expected_summary, actual_summary) |
415 actual_files.pop('summary.json') | 495 actual_files.pop('summary.json') |
416 self.assertEqual(expected_files, actual_files) | 496 self.assertEqual(expected_files, actual_files) |
417 return bot_version | 497 return bot_version |
418 | 498 |
419 | 499 |
420 def cleanup(bot, client, servers, print_all): | 500 def cleanup(bot, client, servers, print_all, leak): |
421 """Kills bot, kills server, print logs if failed, delete tmpdir.""" | 501 """Kills bot, kills server, print logs if failed, delete tmpdir.""" |
422 try: | 502 try: |
423 try: | 503 try: |
424 try: | 504 try: |
425 if bot: | 505 if bot: |
426 bot.stop() | 506 bot.stop(leak) |
427 finally: | 507 finally: |
428 if servers: | 508 if servers: |
429 servers.stop() | 509 servers.stop(leak) |
430 finally: | 510 finally: |
431 if print_all: | 511 if print_all: |
432 if bot: | 512 if bot: |
433 bot.dump_log() | 513 bot.dump_log() |
434 if servers: | 514 if servers: |
435 servers.dump_log() | 515 servers.dump_log() |
436 if client: | 516 if client: |
437 client.dump_log() | 517 client.dump_log() |
438 finally: | 518 finally: |
439 if client: | 519 if client and not leak: |
440 client.cleanup() | 520 client.cleanup() |
441 | 521 |
442 | 522 |
443 def main(): | 523 def main(): |
444 verbose = '-v' in sys.argv | 524 verbose = '-v' in sys.argv |
| 525 leak = bool('--leak' in sys.argv) |
| 526 if leak: |
| 527 sys.argv.remove('--leak') |
445 if verbose: | 528 if verbose: |
446 logging.basicConfig(level=logging.INFO) | 529 logging.basicConfig(level=logging.INFO) |
447 unittest.TestCase.maxDiff = None | 530 unittest.TestCase.maxDiff = None |
448 else: | 531 else: |
449 logging.basicConfig(level=logging.ERROR) | 532 logging.basicConfig(level=logging.ERROR) |
450 | 533 |
451 # Force language to be English, otherwise the error messages differ from | 534 # Force language to be English, otherwise the error messages differ from |
452 # expectations. | 535 # expectations. |
453 os.environ['LANG'] = 'en_US.UTF-8' | 536 os.environ['LANG'] = 'en_US.UTF-8' |
454 os.environ['LANGUAGE'] = 'en_US.UTF-8' | 537 os.environ['LANGUAGE'] = 'en_US.UTF-8' |
(...skipping 12 matching lines...) Expand all Loading... |
467 # Test cases only interract with the client; except for test_update_continue | 550 # Test cases only interract with the client; except for test_update_continue |
468 # which mutates the bot. | 551 # which mutates the bot. |
469 Test.client = client | 552 Test.client = client |
470 Test.servers = servers | 553 Test.servers = servers |
471 failed = not unittest.main(exit=False).result.wasSuccessful() | 554 failed = not unittest.main(exit=False).result.wasSuccessful() |
472 except KeyboardInterrupt: | 555 except KeyboardInterrupt: |
473 print >> sys.stderr, '<Ctrl-C>' | 556 print >> sys.stderr, '<Ctrl-C>' |
474 if bot: | 557 if bot: |
475 bot.kill() | 558 bot.kill() |
476 finally: | 559 finally: |
477 cleanup(bot, client, servers, failed or verbose) | 560 cleanup(bot, client, servers, failed or verbose, leak) |
478 return int(failed) | 561 return int(failed) |
479 | 562 |
480 | 563 |
481 if __name__ == '__main__': | 564 if __name__ == '__main__': |
482 sys.exit(main()) | 565 sys.exit(main()) |
OLD | NEW |