| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2014 The LUCI Authors. All rights reserved. | 2 # Copyright 2014 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be 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 """ |
| 11 | 11 |
| 12 import base64 | 12 import base64 |
| 13 import json | 13 import json |
| 14 import logging | 14 import logging |
| 15 import os | 15 import os |
| 16 import re | 16 import re |
| 17 import signal | 17 import signal |
| 18 import socket | 18 import socket |
| 19 import sys | 19 import sys |
| 20 import tempfile | 20 import tempfile |
| 21 import textwrap | 21 import textwrap |
| 22 import time |
| 22 import unittest | 23 import unittest |
| 23 import urllib | 24 import urllib |
| 24 | 25 |
| 25 APP_DIR = os.path.dirname(os.path.abspath(__file__)) | 26 APP_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 26 BOT_DIR = os.path.join(APP_DIR, 'swarming_bot') | 27 BOT_DIR = os.path.join(APP_DIR, 'swarming_bot') |
| 27 CLIENT_DIR = os.path.join(APP_DIR, '..', '..', 'client') | 28 CLIENT_DIR = os.path.join(APP_DIR, '..', '..', 'client') |
| 28 | 29 |
| 29 from tools import start_bot | 30 from tools import start_bot |
| 30 from tools import start_servers | 31 from tools import start_servers |
| 31 | 32 |
| (...skipping 169 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 201 os.remove(tmp) | 202 os.remove(tmp) |
| 202 | 203 |
| 203 def terminate(self, bot_id): | 204 def terminate(self, bot_id): |
| 204 return self._run('terminate', ['--wait', bot_id]) | 205 return self._run('terminate', ['--wait', bot_id]) |
| 205 | 206 |
| 206 def cleanup(self): | 207 def cleanup(self): |
| 207 if self._tmpdir: | 208 if self._tmpdir: |
| 208 file_path.rmtree(self._tmpdir) | 209 file_path.rmtree(self._tmpdir) |
| 209 self._tmpdir = None | 210 self._tmpdir = None |
| 210 | 211 |
| 212 def query_bot(self): |
| 213 """Returns the bot's properties.""" |
| 214 data = json.loads(self._capture('query', ['bots/list', '--limit', '10'])) |
| 215 if not data.get('items'): |
| 216 return None |
| 217 assert len(data['items']) == 1 |
| 218 return data['items'][0] |
| 219 |
| 211 def dump_log(self): | 220 def dump_log(self): |
| 212 print >> sys.stderr, '-' * 60 | 221 print >> sys.stderr, '-' * 60 |
| 213 print >> sys.stderr, 'Client calls' | 222 print >> sys.stderr, 'Client calls' |
| 214 print >> sys.stderr, '-' * 60 | 223 print >> sys.stderr, '-' * 60 |
| 215 for i in xrange(self._index): | 224 for i in xrange(self._index): |
| 216 with open(os.path.join(self._tmpdir, 'client_%d.log' % i), 'rb') as f: | 225 with open(os.path.join(self._tmpdir, 'client_%d.log' % i), 'rb') as f: |
| 217 log = f.read().strip('\n') | 226 log = f.read().strip('\n') |
| 218 for l in log.splitlines(): | 227 for l in log.splitlines(): |
| 219 sys.stderr.write(' %s\n' % l) | 228 sys.stderr.write(' %s\n' % l) |
| 220 | 229 |
| (...skipping 13 matching lines...) Expand all Loading... |
| 234 '--verbose', | 243 '--verbose', |
| 235 ] + args | 244 ] + args |
| 236 with open(name, 'wb') as f: | 245 with open(name, 'wb') as f: |
| 237 f.write('\nRunning: %s\n' % ' '.join(cmd)) | 246 f.write('\nRunning: %s\n' % ' '.join(cmd)) |
| 238 f.flush() | 247 f.flush() |
| 239 p = subprocess42.Popen( | 248 p = subprocess42.Popen( |
| 240 cmd, stdout=f, stderr=subprocess42.STDOUT, cwd=CLIENT_DIR) | 249 cmd, stdout=f, stderr=subprocess42.STDOUT, cwd=CLIENT_DIR) |
| 241 p.communicate() | 250 p.communicate() |
| 242 return p.returncode | 251 return p.returncode |
| 243 | 252 |
| 253 def _capture(self, command, args): |
| 254 cmd = [ |
| 255 sys.executable, 'swarming.py', command, '-S', self._swarming_server, |
| 256 ] + args |
| 257 p = subprocess42.Popen(cmd, stdout=subprocess42.PIPE, cwd=CLIENT_DIR) |
| 258 return p.communicate()[0] |
| 259 |
| 244 | 260 |
| 245 def gen_expected(**kwargs): | 261 def gen_expected(**kwargs): |
| 246 expected = { | 262 expected = { |
| 247 u'abandoned_ts': None, | 263 u'abandoned_ts': None, |
| 248 u'bot_dimensions': None, | 264 u'bot_dimensions': None, |
| 249 u'bot_id': unicode(socket.getfqdn().split('.', 1)[0]), | 265 u'bot_id': unicode(socket.getfqdn().split('.', 1)[0]), |
| 250 u'children_task_ids': [], | 266 u'children_task_ids': [], |
| 251 u'cost_saved_usd': None, | 267 u'cost_saved_usd': None, |
| 252 u'deduped_from': None, | 268 u'deduped_from': None, |
| 253 u'exit_codes': [0], | 269 u'exit_codes': [0], |
| (...skipping 19 matching lines...) Expand all Loading... |
| 273 # convert_to_old_format.py in //client/swarming.py. | 289 # convert_to_old_format.py in //client/swarming.py. |
| 274 keys = set(expected) | {u'performance_stats'} | 290 keys = set(expected) | {u'performance_stats'} |
| 275 assert keys.issuperset(kwargs) | 291 assert keys.issuperset(kwargs) |
| 276 expected.update({unicode(k): v for k, v in kwargs.iteritems()}) | 292 expected.update({unicode(k): v for k, v in kwargs.iteritems()}) |
| 277 return expected | 293 return expected |
| 278 | 294 |
| 279 | 295 |
| 280 class Test(unittest.TestCase): | 296 class Test(unittest.TestCase): |
| 281 maxDiff = None | 297 maxDiff = None |
| 282 client = None | 298 client = None |
| 283 dimensions = None | |
| 284 servers = None | 299 servers = None |
| 285 bot = None | 300 bot = None |
| 286 | 301 |
| 287 @classmethod | |
| 288 def setUpClass(cls): | |
| 289 cls.dimensions = os_utilities.get_dimensions() | |
| 290 | |
| 291 def setUp(self): | 302 def setUp(self): |
| 292 super(Test, self).setUp() | 303 super(Test, self).setUp() |
| 293 # Reset the bot's cache at the start of each task, so that the cache reuse | 304 self.dimensions = os_utilities.get_dimensions() |
| 294 # data becomes deterministic. | 305 # Reset the bot's isolated cache at the start of each task, so that the |
| 295 # Main caveat is 'isolated_upload' as the isolate server is not cleared. | 306 # cache reuse data becomes deterministic. Only restart the bot when it had a |
| 296 self.bot.wipe_cache() | 307 # named cache because it takes multiple seconds to to restart the bot. :( |
| 308 # |
| 309 # TODO(maruel): 'isolated_upload' is not deterministic because the isolate |
| 310 # server not cleared. |
| 311 old = self.client.query_bot() |
| 312 started_ts = json.loads(old['state'])['started_ts'] if old else None |
| 313 logging.info('setUp: started_ts was %s', started_ts) |
| 314 had_cache = any( |
| 315 u'caches' == i['key'] for i in old['dimensions']) if old else False |
| 316 self.bot.wipe_cache(had_cache) |
| 317 # The bot restarts due to wipe_cache() so wait for the bot to come back |
| 318 # online. It may takes a few loop. |
| 319 while True: |
| 320 state = self.client.query_bot() |
| 321 if not state: |
| 322 time.sleep(0.1) |
| 323 continue |
| 324 if not had_cache: |
| 325 break |
| 326 new_started_ts = json.loads(state['state'])['started_ts'] |
| 327 logging.info('setUp: new_started_ts is %s', new_started_ts) |
| 328 # This assumes that starting the bot and running the previous test case |
| 329 # took more than 1s. |
| 330 if not started_ts or new_started_ts != started_ts: |
| 331 dimensions = {i['key']: i['value'] for i in state['dimensions']} |
| 332 self.assertNotIn(u'caches', dimensions) |
| 333 break |
| 297 | 334 |
| 298 def gen_expected(self, **kwargs): | 335 def gen_expected(self, **kwargs): |
| 299 return gen_expected(bot_dimensions=self.dimensions, **kwargs) | 336 return gen_expected(bot_dimensions=self.dimensions, **kwargs) |
| 300 | 337 |
| 301 def test_raw_bytes(self): | 338 def test_raw_bytes(self): |
| 302 # A string of a letter 'A', UTF-8 BOM then UTF-16 BOM then UTF-EDBCDIC then | 339 # A string of a letter 'A', UTF-8 BOM then UTF-16 BOM then UTF-EDBCDIC then |
| 303 # invalid UTF-8 and the letter 'B'. It is double escaped so it can be passed | 340 # invalid UTF-8 and the letter 'B'. It is double escaped so it can be passed |
| 304 # down the shell. | 341 # down the shell. |
| 305 invalid_bytes = 'A\\xEF\\xBB\\xBF\\xFE\\xFF\\xDD\\x73\\x66\\x73\\xc3\\x28B' | 342 invalid_bytes = 'A\\xEF\\xBB\\xBF\\xFE\\xFF\\xDD\\x73\\x66\\x73\\xc3\\x28B' |
| 306 args = [ | 343 args = [ |
| (...skipping 322 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 629 self._run_isolated( | 666 self._run_isolated( |
| 630 hello_world, 'secret_bytes', | 667 hello_world, 'secret_bytes', |
| 631 ['--secret-bytes-path', tmp, '--', '${ISOLATED_OUTDIR}'], | 668 ['--secret-bytes-path', tmp, '--', '${ISOLATED_OUTDIR}'], |
| 632 expected_summary, {os.path.join('0', 'sekret'): 'foobar\n'}) | 669 expected_summary, {os.path.join('0', 'sekret'): 'foobar\n'}) |
| 633 finally: | 670 finally: |
| 634 os.remove(tmp) | 671 os.remove(tmp) |
| 635 | 672 |
| 636 def test_local_cache(self): | 673 def test_local_cache(self): |
| 637 # First task creates the cache, second copy the content to the output | 674 # First task creates the cache, second copy the content to the output |
| 638 # directory. Each time it's the exact same script. | 675 # directory. Each time it's the exact same script. |
| 676 dimensions = { |
| 677 i['key']: i['value'] for i in self.client.query_bot()['dimensions']} |
| 678 self.assertEqual(set(self.dimensions), set(dimensions)) |
| 679 self.assertNotIn(u'cache', set(dimensions)) |
| 639 script = '\n'.join(( | 680 script = '\n'.join(( |
| 640 'import os, shutil, sys', | 681 'import os, shutil, sys', |
| 641 'p = "p/b/a.txt"', | 682 'p = "p/b/a.txt"', |
| 642 'if not os.path.isfile(p):', | 683 'if not os.path.isfile(p):', |
| 643 ' with open(p, "wb") as f:', | 684 ' with open(p, "wb") as f:', |
| 644 ' f.write("Yo!")', | 685 ' f.write("Yo!")', |
| 645 'else:', | 686 'else:', |
| 646 ' shutil.copy(p, sys.argv[1])', | 687 ' shutil.copy(p, sys.argv[1])', |
| 647 'print "hi"')) | 688 'print "hi"')) |
| 648 sizes = sorted([len(script), 200]) | 689 sizes = sorted([len(script), 200]) |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 685 u'initial_size': unicode(sum(sizes)), | 726 u'initial_size': unicode(sum(sizes)), |
| 686 u'items_cold': [], | 727 u'items_cold': [], |
| 687 u'items_hot': sizes, | 728 u'items_hot': sizes, |
| 688 }, | 729 }, |
| 689 u'isolated_upload': { | 730 u'isolated_upload': { |
| 690 u'items_cold': [3, 110], | 731 u'items_cold': [3, 110], |
| 691 u'items_hot': [], | 732 u'items_hot': [], |
| 692 }, | 733 }, |
| 693 }, | 734 }, |
| 694 ) | 735 ) |
| 736 # The previous task caused the bot to have a named cache. |
| 737 expected_summary['bot_dimensions'] = ( |
| 738 expected_summary['bot_dimensions'].copy()) |
| 739 expected_summary['bot_dimensions'][u'caches'] = [u'fuu'] |
| 695 self._run_isolated( | 740 self._run_isolated( |
| 696 script, 'cache_second', | 741 script, 'cache_second', |
| 697 ['--named-cache', 'fuu', 'p/b', '--', '${ISOLATED_OUTDIR}/yo'], | 742 ['--named-cache', 'fuu', 'p/b', '--', '${ISOLATED_OUTDIR}/yo'], |
| 698 expected_summary, | 743 expected_summary, |
| 699 {'0/yo': 'Yo!'}) | 744 {'0/yo': 'Yo!'}) |
| 700 | 745 |
| 746 # Check that the bot now has a cache dimension by independently querying. |
| 747 expected = set(self.dimensions) |
| 748 expected.add(u'caches') |
| 749 dimensions = { |
| 750 i['key']: i['value'] for i in self.client.query_bot()['dimensions']} |
| 751 self.assertEqual(expected, set(dimensions)) |
| 752 |
| 701 def _run_isolated(self, hello_world, name, args, expected_summary, | 753 def _run_isolated(self, hello_world, name, args, expected_summary, |
| 702 expected_files, deduped=False, isolated_content=None): | 754 expected_files, deduped=False, isolated_content=None): |
| 703 """Runs hello_world.py as an isolated file.""" | 755 """Runs hello_world.py as an isolated file.""" |
| 704 # Shared code for all test_isolated_* test cases. | 756 # Shared code for all test_isolated_* test cases. |
| 705 tmpdir = tempfile.mkdtemp(prefix='swarming_smoke') | 757 tmpdir = tempfile.mkdtemp(prefix='swarming_smoke') |
| 706 try: | 758 try: |
| 707 isolate_path = os.path.join(tmpdir, 'i.isolate') | 759 isolate_path = os.path.join(tmpdir, 'i.isolate') |
| 708 isolated_path = os.path.join(tmpdir, 'i.isolated') | 760 isolated_path = os.path.join(tmpdir, 'i.isolated') |
| 709 with open(isolate_path, 'wb') as f: | 761 with open(isolate_path, 'wb') as f: |
| 710 json.dump(isolated_content or ISOLATE_HELLO_WORLD, f) | 762 json.dump(isolated_content or ISOLATE_HELLO_WORLD, f) |
| (...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 810 finally: | 862 finally: |
| 811 if client and not leak: | 863 if client and not leak: |
| 812 client.cleanup() | 864 client.cleanup() |
| 813 | 865 |
| 814 | 866 |
| 815 def main(): | 867 def main(): |
| 816 fix_encoding.fix_encoding() | 868 fix_encoding.fix_encoding() |
| 817 verbose = '-v' in sys.argv | 869 verbose = '-v' in sys.argv |
| 818 leak = bool('--leak' in sys.argv) | 870 leak = bool('--leak' in sys.argv) |
| 819 if leak: | 871 if leak: |
| 872 # Note that --leak will not guarantee that 'c' and 'isolated_cache' are |
| 873 # kept. Only the last test case will leak these two directories. |
| 820 sys.argv.remove('--leak') | 874 sys.argv.remove('--leak') |
| 821 if verbose: | 875 if verbose: |
| 822 logging.basicConfig(level=logging.INFO) | 876 logging.basicConfig(level=logging.INFO) |
| 823 Test.maxDiff = None | 877 Test.maxDiff = None |
| 824 else: | 878 else: |
| 825 logging.basicConfig(level=logging.ERROR) | 879 logging.basicConfig(level=logging.ERROR) |
| 826 | 880 |
| 827 # Force language to be English, otherwise the error messages differ from | 881 # Force language to be English, otherwise the error messages differ from |
| 828 # expectations. | 882 # expectations. |
| 829 os.environ['LANG'] = 'en_US.UTF-8' | 883 os.environ['LANG'] = 'en_US.UTF-8' |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 870 if bot is not None and bot.poll() is None: | 924 if bot is not None and bot.poll() is None: |
| 871 bot.kill() | 925 bot.kill() |
| 872 bot.wait() | 926 bot.wait() |
| 873 finally: | 927 finally: |
| 874 cleanup(bot, client, servers, failed or verbose, leak) | 928 cleanup(bot, client, servers, failed or verbose, leak) |
| 875 return int(failed) | 929 return int(failed) |
| 876 | 930 |
| 877 | 931 |
| 878 if __name__ == '__main__': | 932 if __name__ == '__main__': |
| 879 sys.exit(main()) | 933 sys.exit(main()) |
| OLD | NEW |