Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1084)

Side by Side Diff: appengine/swarming/local_smoke_test.py

Issue 2911193003: Expose named caches as dimensions. (Closed)
Patch Set: Fixed smoke test Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | appengine/swarming/swarming_bot/api/os_utilities.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
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())
OLDNEW
« no previous file with comments | « no previous file | appengine/swarming/swarming_bot/api/os_utilities.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698