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.
6 # pylint: disable=R0201
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'))
25 from depot_tools import auto_stub
30 def write_content(filepath, content):
31 with open(filepath, 'wb') as f:
36 return json.dumps(data, sort_keys=True, separators=(',', ':'))
39 class StorageFake(object):
40 def __init__(self, files):
41 self._files = files.copy()
43 def __enter__(self, *_):
46 def __exit__(self, *_):
53 def async_fetch(self, channel, _priority, digest, _size, sink):
54 sink([self._files[digest]])
55 channel.send_result(digest)
58 class RunIsolatedTest(auto_stub.TestCase):
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)
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()
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
81 (filepath, oct(expected), oct(actual), oct(umask)))
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)
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')
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
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)
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):
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.
139 with self.assertRaises(OSError):
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
157 def test_hard_link_mode(self):
158 # Creates a hard link, see if the file mode changed on the node or the
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
177 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
180 # pylint: disable=W0613
181 def call(command, cwd, env):
182 calls.append(command)
184 self.mock(run_isolated.subprocess, 'call', call)
185 isolated = json_dumps(
187 'command': ['foo.exe', 'cmd with space'],
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)
196 '--hash', isolated_hash,
197 '--cache', self.tempdir,
198 '--isolate-server', 'https://localhost',
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)
204 def test_main_args(self):
205 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
208 # pylint: disable=W0613
209 def call(command, cwd, env):
210 calls.append(command)
212 self.mock(run_isolated.subprocess, 'call', call)
213 isolated = json_dumps(
215 'command': ['foo.exe', 'cmd with space'],
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)
224 '--hash', isolated_hash,
225 '--cache', self.tempdir,
226 '--isolate-server', 'https://localhost',
231 ret = run_isolated.main(cmd)
232 self.assertEqual(0, ret)
234 [[self.temp_join(u'foo.exe'), u'cmd with space', '--extraargs', 'bar']],
237 def _run_tha_test(self, isolated_hash, files):
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))
245 # Keeps tuple of (args, kwargs).
248 run_isolated.subprocess, 'call',
249 lambda *x, **y: subprocess_call.append((x, y)) or 0)
251 ret = run_isolated.run_tha_test(
254 run_isolated.isolateserver.MemoryCache(),
256 self.assertEqual(0, ret)
257 return subprocess_call, make_tree_call
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)
265 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
271 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
273 def test_run_tha_test_naked_read_only_0(self):
274 isolated = json_dumps(
276 'command': ['invalid', 'command'],
279 isolated_hash = ALGO(isolated).hexdigest()
280 files = {isolated_hash:isolated}
281 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
283 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
289 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
291 def test_run_tha_test_naked_read_only_1(self):
292 isolated = json_dumps(
294 'command': ['invalid', 'command'],
297 isolated_hash = ALGO(isolated).hexdigest()
298 files = {isolated_hash:isolated}
299 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
302 'make_tree_files_read_only', 'make_tree_deleteable',
303 'make_tree_deleteable',
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'))
310 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
312 def test_run_tha_test_naked_read_only_2(self):
313 isolated = json_dumps(
315 'command': ['invalid', 'command'],
318 isolated_hash = ALGO(isolated).hexdigest()
319 files = {isolated_hash:isolated}
320 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
322 ['make_tree_read_only', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
328 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
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)
339 # Keeps tuple of (args, kwargs).
342 run_isolated.subprocess, 'call',
343 lambda *x, **y: subprocess_call.append((x, y)) or 8)
347 '--hash', isolated_hash,
348 '--cache', self.tempdir,
349 '--isolate-server', 'https://localhost',
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'))
357 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
359 def test_modified_cwd(self):
360 isolated = json_dumps({
361 'command': ['../out/some.exe', 'arg'],
362 'relative_cwd': 'some',
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'))
371 [(([self.temp_join(u'out', u'some.exe'), 'arg'],), {})],
374 def test_python_cmd(self):
375 isolated = json_dumps({
376 'command': ['../out/cmd.py', 'arg'],
377 'relative_cwd': 'some',
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.
387 [(([sys.executable, os.path.join('..', 'out', 'cmd.py'), 'arg'],), {})],
390 def test_output(self):
393 'open(sys.argv[1], "w").write("bar")\n')
394 script_hash = ALGO(script).hexdigest()
395 isolated = json_dumps(
398 'command': ['cmd.py', '${ISOLATED_OUTDIR}/foo'],
406 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
408 isolated_hash = ALGO(isolated).hexdigest()
410 isolated_hash: isolated,
414 path = os.path.join(self.tempdir, 'store')
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')
420 self.mock(sys, 'stdout', StringIO.StringIO())
421 ret = run_isolated.run_tha_test(
424 run_isolated.isolateserver.MemoryCache(),
426 self.assertEqual(0, ret)
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(
438 # TODO(maruel): Handle umask.
443 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
445 uploaded_hash = ALGO(uploaded).hexdigest()
446 hashes.add(uploaded_hash)
447 self.assertEqual(hashes, set(os.listdir(path)))
450 '[run_isolated_out_hack]',
451 '{"hash":"%s","namespace":"default-store","storage":"%s"}' % (
452 uploaded_hash, path),
453 '[/run_isolated_out_hack]'
455 self.assertEqual(expected, sys.stdout.getvalue())
457 if __name__ == '__main__':
459 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)