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