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