Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / tests / run_isolated_test.py
1 #!/usr/bin/env python
2 # Copyright 2013 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
5
6 import StringIO
7 import functools
8 import hashlib
9 import json
10 import logging
11 import os
12 import shutil
13 import sys
14 import tempfile
15 import unittest
16
17 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18 sys.path.insert(0, ROOT_DIR)
19 sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party'))
20
21 import run_isolated
22 import test_utils
23 from depot_tools import auto_stub
24
25 ALGO = hashlib.sha1
26
27
28 def write_content(filepath, content):
29   with open(filepath, 'wb') as f:
30     f.write(content)
31
32
33 def json_dumps(data):
34   return json.dumps(data, sort_keys=True, separators=(',', ':'))
35
36
37 class StorageFake(object):
38   def __init__(self, files):
39     self._files = files.copy()
40
41   def __enter__(self, *_):
42     return self
43
44   def __exit__(self, *_):
45     pass
46
47   def async_fetch(self, channel, _priority, digest, _size, sink):
48     sink([self._files[digest]])
49     channel.send_result(digest)
50
51
52 class RunIsolatedTest(auto_stub.TestCase):
53   def setUp(self):
54     super(RunIsolatedTest, self).setUp()
55     self.tempdir = tempfile.mkdtemp(prefix='run_isolated_test')
56     logging.debug(self.tempdir)
57     self.mock(run_isolated, 'make_temp_dir', self.fake_make_temp_dir)
58
59   def tearDown(self):
60     for dirpath, dirnames, filenames in os.walk(self.tempdir, topdown=True):
61       for filename in filenames:
62         run_isolated.set_read_only(os.path.join(dirpath, filename), False)
63       for dirname in dirnames:
64         run_isolated.set_read_only(os.path.join(dirpath, dirname), False)
65     shutil.rmtree(self.tempdir)
66     super(RunIsolatedTest, self).tearDown()
67
68   def assertFileMode(self, filepath, mode, umask=None):
69     umask = test_utils.umask() if umask is None else umask
70     actual = os.stat(filepath).st_mode
71     expected = mode & ~umask
72     self.assertEqual(
73         expected,
74         actual,
75         (filepath, oct(expected), oct(actual), oct(umask)))
76
77   def assertMaskedFileMode(self, filepath, mode):
78     """It's usually when the file was first marked read only."""
79     self.assertFileMode(filepath, mode, 0 if sys.platform == 'win32' else 077)
80
81   @property
82   def run_test_temp_dir(self):
83     """Where to map all files in run_isolated.run_tha_test."""
84     return os.path.join(self.tempdir, 'run_tha_test')
85
86   def fake_make_temp_dir(self, _prefix=None, _root_dir=None):
87     """Predictably returns directory for run_tha_test (one per test case)."""
88     assert not os.path.isdir(self.run_test_temp_dir)
89     os.makedirs(self.run_test_temp_dir)
90     return self.run_test_temp_dir
91
92   def temp_join(self, *args):
93     """Shortcut for joining path with self.run_test_temp_dir."""
94     return os.path.join(self.run_test_temp_dir, *args)
95
96   def test_delete_wd_rf(self):
97     # Confirms that a RO file in a RW directory can be deleted on non-Windows.
98     dir_foo = os.path.join(self.tempdir, 'foo')
99     file_bar = os.path.join(dir_foo, 'bar')
100     os.mkdir(dir_foo, 0777)
101     write_content(file_bar, 'bar')
102     run_isolated.set_read_only(dir_foo, False)
103     run_isolated.set_read_only(file_bar, True)
104     self.assertFileMode(dir_foo, 040777)
105     self.assertMaskedFileMode(file_bar, 0100444)
106     if sys.platform == 'win32':
107       # On Windows, a read-only file can't be deleted.
108       with self.assertRaises(OSError):
109         os.remove(file_bar)
110     else:
111       os.remove(file_bar)
112
113   def test_delete_rd_wf(self):
114     # Confirms that a Rw file in a RO directory can be deleted on Windows only.
115     dir_foo = os.path.join(self.tempdir, 'foo')
116     file_bar = os.path.join(dir_foo, 'bar')
117     os.mkdir(dir_foo, 0777)
118     write_content(file_bar, 'bar')
119     run_isolated.set_read_only(dir_foo, True)
120     run_isolated.set_read_only(file_bar, False)
121     self.assertMaskedFileMode(dir_foo, 040555)
122     self.assertFileMode(file_bar, 0100666)
123     if sys.platform == 'win32':
124       # A read-only directory has a convoluted meaning on Windows, it means that
125       # the directory is "personalized". This is used as a signal by Windows
126       # Explorer to tell it to look into the directory for desktop.ini.
127       # See http://support.microsoft.com/kb/326549 for more details.
128       # As such, it is important to not try to set the read-only bit on
129       # directories on Windows since it has no effect other than trigger
130       # Windows Explorer to look for desktop.ini, which is unnecessary.
131       os.remove(file_bar)
132     else:
133       with self.assertRaises(OSError):
134         os.remove(file_bar)
135
136   def test_delete_rd_rf(self):
137     # Confirms that a RO file in a RO directory can't be deleted.
138     dir_foo = os.path.join(self.tempdir, 'foo')
139     file_bar = os.path.join(dir_foo, 'bar')
140     os.mkdir(dir_foo, 0777)
141     write_content(file_bar, 'bar')
142     run_isolated.set_read_only(dir_foo, True)
143     run_isolated.set_read_only(file_bar, True)
144     self.assertMaskedFileMode(dir_foo, 040555)
145     self.assertMaskedFileMode(file_bar, 0100444)
146     with self.assertRaises(OSError):
147       # It fails for different reason depending on the OS. See the test cases
148       # above.
149       os.remove(file_bar)
150
151   def test_hard_link_mode(self):
152     # Creates a hard link, see if the file mode changed on the node or the
153     # directory entry.
154     dir_foo = os.path.join(self.tempdir, 'foo')
155     file_bar = os.path.join(dir_foo, 'bar')
156     file_link = os.path.join(dir_foo, 'link')
157     os.mkdir(dir_foo, 0777)
158     write_content(file_bar, 'bar')
159     run_isolated.hardlink(file_bar, file_link)
160     self.assertFileMode(file_bar, 0100666)
161     self.assertFileMode(file_link, 0100666)
162     run_isolated.set_read_only(file_bar, True)
163     self.assertMaskedFileMode(file_bar, 0100444)
164     self.assertMaskedFileMode(file_link, 0100444)
165     # This is bad news for Windows; on Windows, the file must be writeable to be
166     # deleted, but the file node is modified. This means that every hard links
167     # must be reset to be read-only after deleting one of the hard link
168     # directory entry.
169
170   def test_main(self):
171     self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
172     calls = []
173     # Unused argument ''
174     # pylint: disable=W0613
175     def call(command, cwd, env):
176       calls.append(command)
177       return 0
178     self.mock(run_isolated.subprocess, 'call', call)
179     isolated = json_dumps(
180         {
181           'command': ['foo.exe', 'cmd with space'],
182         })
183     isolated_hash = ALGO(isolated).hexdigest()
184     def get_storage(_isolate_server, _namespace):
185       return StorageFake({isolated_hash:isolated})
186     self.mock(run_isolated.isolateserver, 'get_storage', get_storage)
187
188     cmd = [
189         '--no-log',
190         '--hash', isolated_hash,
191         '--cache', self.tempdir,
192         '--isolate-server', 'https://localhost',
193     ]
194     ret = run_isolated.main(cmd)
195     self.assertEqual(0, ret)
196     self.assertEqual([[self.temp_join(u'foo.exe'), u'cmd with space']], calls)
197
198   def test_main_args(self):
199     self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
200     calls = []
201     # Unused argument ''
202     # pylint: disable=W0613
203     def call(command, cwd, env):
204       calls.append(command)
205       return 0
206     self.mock(run_isolated.subprocess, 'call', call)
207     isolated = json_dumps(
208         {
209           'command': ['foo.exe', 'cmd with space'],
210         })
211     isolated_hash = ALGO(isolated).hexdigest()
212     def get_storage(_isolate_server, _namespace):
213       return StorageFake({isolated_hash:isolated})
214     self.mock(run_isolated.isolateserver, 'get_storage', get_storage)
215
216     cmd = [
217         '--no-log',
218         '--hash', isolated_hash,
219         '--cache', self.tempdir,
220         '--isolate-server', 'https://localhost',
221         '--',
222         '--extraargs',
223         'bar',
224     ]
225     ret = run_isolated.main(cmd)
226     self.assertEqual(0, ret)
227     self.assertEqual(
228         [[self.temp_join(u'foo.exe'), u'cmd with space', '--extraargs', 'bar']],
229         calls)
230
231   def _run_tha_test(self, isolated_hash, files):
232     make_tree_call = []
233     def add(i, _):
234       make_tree_call.append(i)
235     for i in ('make_tree_read_only', 'make_tree_files_read_only',
236               'make_tree_deleteable', 'make_tree_writeable'):
237       self.mock(run_isolated, i, functools.partial(add, i))
238
239     # Keeps tuple of (args, kwargs).
240     subprocess_call = []
241     self.mock(
242         run_isolated.subprocess, 'call',
243         lambda *x, **y: subprocess_call.append((x, y)) or 0)
244
245     ret = run_isolated.run_tha_test(
246         isolated_hash,
247         StorageFake(files),
248         run_isolated.isolateserver.MemoryCache(),
249         run_isolated.isolateserver.get_hash_algo('default-deflate'),
250         [])
251     self.assertEqual(0, ret)
252     return subprocess_call, make_tree_call
253
254   def test_run_tha_test_naked(self):
255     isolated = json_dumps({'command': ['invalid', 'command']})
256     isolated_hash = ALGO(isolated).hexdigest()
257     files = {isolated_hash:isolated}
258     subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
259     self.assertEqual(
260         ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
261         make_tree_call)
262     self.assertEqual(1, len(subprocess_call))
263     self.assertTrue(subprocess_call[0][1].pop('cwd'))
264     self.assertTrue(subprocess_call[0][1].pop('env'))
265     self.assertEqual(
266         [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
267
268   def test_run_tha_test_naked_read_only_0(self):
269     isolated = json_dumps(
270         {
271           'command': ['invalid', 'command'],
272           'read_only': 0,
273         })
274     isolated_hash = ALGO(isolated).hexdigest()
275     files = {isolated_hash:isolated}
276     subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
277     self.assertEqual(
278         ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
279         make_tree_call)
280     self.assertEqual(1, len(subprocess_call))
281     self.assertTrue(subprocess_call[0][1].pop('cwd'))
282     self.assertTrue(subprocess_call[0][1].pop('env'))
283     self.assertEqual(
284         [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
285
286   def test_run_tha_test_naked_read_only_1(self):
287     isolated = json_dumps(
288         {
289           'command': ['invalid', 'command'],
290           'read_only': 1,
291         })
292     isolated_hash = ALGO(isolated).hexdigest()
293     files = {isolated_hash:isolated}
294     subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
295     self.assertEqual(
296         [
297           'make_tree_files_read_only', 'make_tree_deleteable',
298           'make_tree_deleteable',
299         ],
300         make_tree_call)
301     self.assertEqual(1, len(subprocess_call))
302     self.assertTrue(subprocess_call[0][1].pop('cwd'))
303     self.assertTrue(subprocess_call[0][1].pop('env'))
304     self.assertEqual(
305         [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
306
307   def test_run_tha_test_naked_read_only_2(self):
308     isolated = json_dumps(
309         {
310           'command': ['invalid', 'command'],
311           'read_only': 2,
312         })
313     isolated_hash = ALGO(isolated).hexdigest()
314     files = {isolated_hash:isolated}
315     subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
316     self.assertEqual(
317         ['make_tree_read_only', 'make_tree_deleteable', 'make_tree_deleteable'],
318         make_tree_call)
319     self.assertEqual(1, len(subprocess_call))
320     self.assertTrue(subprocess_call[0][1].pop('cwd'))
321     self.assertTrue(subprocess_call[0][1].pop('env'))
322     self.assertEqual(
323         [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
324
325   def test_main_naked(self):
326     # The most naked .isolated file that can exist.
327     self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
328     isolated = json_dumps({'command': ['invalid', 'command']})
329     isolated_hash = ALGO(isolated).hexdigest()
330     def get_storage(_isolate_server, _namespace):
331       return StorageFake({isolated_hash:isolated})
332     self.mock(run_isolated.isolateserver, 'get_storage', get_storage)
333
334     # Keeps tuple of (args, kwargs).
335     subprocess_call = []
336     self.mock(
337         run_isolated.subprocess, 'call',
338         lambda *x, **y: subprocess_call.append((x, y)) or 8)
339
340     cmd = [
341         '--no-log',
342         '--hash', isolated_hash,
343         '--cache', self.tempdir,
344         '--isolate-server', 'https://localhost',
345     ]
346     ret = run_isolated.main(cmd)
347     self.assertEqual(8, ret)
348     self.assertEqual(1, len(subprocess_call))
349     self.assertTrue(subprocess_call[0][1].pop('cwd'))
350     self.assertTrue(subprocess_call[0][1].pop('env'))
351     self.assertEqual(
352         [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
353
354   def test_modified_cwd(self):
355     isolated = json_dumps({
356         'command': ['../out/some.exe', 'arg'],
357         'relative_cwd': 'some',
358     })
359     isolated_hash = ALGO(isolated).hexdigest()
360     files = {isolated_hash:isolated}
361     subprocess_call, _ = self._run_tha_test(isolated_hash, files)
362     self.assertEqual(1, len(subprocess_call))
363     self.assertEqual(subprocess_call[0][1].pop('cwd'), self.temp_join('some'))
364     self.assertTrue(subprocess_call[0][1].pop('env'))
365     self.assertEqual(
366         [(([self.temp_join(u'out', u'some.exe'), 'arg'],), {})],
367         subprocess_call)
368
369   def test_python_cmd(self):
370     isolated = json_dumps({
371         'command': ['../out/cmd.py', 'arg'],
372         'relative_cwd': 'some',
373     })
374     isolated_hash = ALGO(isolated).hexdigest()
375     files = {isolated_hash:isolated}
376     subprocess_call, _ = self._run_tha_test(isolated_hash, files)
377     self.assertEqual(1, len(subprocess_call))
378     self.assertEqual(subprocess_call[0][1].pop('cwd'), self.temp_join('some'))
379     self.assertTrue(subprocess_call[0][1].pop('env'))
380     # Injects sys.executable.
381     self.assertEqual(
382         [(([sys.executable, os.path.join('..', 'out', 'cmd.py'), 'arg'],), {})],
383         subprocess_call)
384
385   def test_output(self):
386     script = (
387       'import sys\n'
388       'open(sys.argv[1], "w").write("bar")\n')
389     script_hash = ALGO(script).hexdigest()
390     isolated = json_dumps(
391         {
392           'algo': 'sha-1',
393           'command': ['cmd.py', '${ISOLATED_OUTDIR}/foo'],
394           'files': {
395             'cmd.py': {
396               'h': script_hash,
397               'm': 0700,
398               's': len(script),
399             },
400           },
401           'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
402         })
403     isolated_hash = ALGO(isolated).hexdigest()
404     contents = {
405         isolated_hash: isolated,
406         script_hash: script,
407     }
408
409     path = os.path.join(self.tempdir, 'store')
410     os.mkdir(path)
411     for h, c in contents.iteritems():
412       write_content(os.path.join(path, h), c)
413     store = run_isolated.isolateserver.get_storage(path, 'default-store')
414
415     self.mock(sys, 'stdout', StringIO.StringIO())
416     ret = run_isolated.run_tha_test(
417         isolated_hash,
418         store,
419         run_isolated.isolateserver.MemoryCache(),
420         run_isolated.isolateserver.get_hash_algo('default-store'),
421         [])
422     self.assertEqual(0, ret)
423
424     # It uploaded back. Assert the store has a new item containing foo.
425     hashes = set(contents)
426     output_hash = ALGO('bar').hexdigest()
427     hashes.add(output_hash)
428     uploaded = json_dumps(
429         {
430           'algo': 'sha-1',
431           'files': {
432             'foo': {
433               'h': output_hash,
434               # TODO(maruel): Handle umask.
435               'm': 0640,
436               's': 3,
437             },
438           },
439           'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
440         })
441     uploaded_hash = ALGO(uploaded).hexdigest()
442     hashes.add(uploaded_hash)
443     self.assertEqual(hashes, set(os.listdir(path)))
444     self.assertEqual(
445         'run_isolated output: %s\n' % uploaded_hash, sys.stdout.getvalue())
446
447
448 if __name__ == '__main__':
449   logging.basicConfig(
450       level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)
451   unittest.main()