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, _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))
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)
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):
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.
141 with self.assertRaises(OSError):
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
159 def test_hard_link_mode(self):
160 # Creates a hard link, see if the file mode changed on the node or the
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
179 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
182 # pylint: disable=W0613
183 def call(command, cwd, env):
184 calls.append(command)
186 self.mock(run_isolated.subprocess, 'call', call)
187 isolated = json_dumps(
189 'command': ['foo.exe', 'cmd with space'],
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)
198 '--hash', isolated_hash,
199 '--cache', self.tempdir,
200 '--isolate-server', 'https://localhost',
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)
206 def test_main_args(self):
207 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
210 # pylint: disable=W0613
211 def call(command, cwd, env):
212 calls.append(command)
214 self.mock(run_isolated.subprocess, 'call', call)
215 isolated = json_dumps(
217 'command': ['foo.exe', 'cmd with space'],
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)
226 '--hash', isolated_hash,
227 '--cache', self.tempdir,
228 '--isolate-server', 'https://localhost',
233 ret = run_isolated.main(cmd)
234 self.assertEqual(0, ret)
236 [[self.temp_join(u'foo.exe'), u'cmd with space', '--extraargs', 'bar']],
239 def _run_tha_test(self, isolated_hash, files):
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))
247 # Keeps tuple of (args, kwargs).
250 run_isolated.subprocess, 'call',
251 lambda *x, **y: subprocess_call.append((x, y)) or 0)
253 ret = run_isolated.run_tha_test(
256 run_isolated.isolateserver.MemoryCache(),
258 self.assertEqual(0, ret)
259 return subprocess_call, make_tree_call
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)
267 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
273 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
275 def test_run_tha_test_naked_read_only_0(self):
276 isolated = json_dumps(
278 'command': ['invalid', 'command'],
281 isolated_hash = ALGO(isolated).hexdigest()
282 files = {isolated_hash:isolated}
283 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
285 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
291 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
293 def test_run_tha_test_naked_read_only_1(self):
294 isolated = json_dumps(
296 'command': ['invalid', 'command'],
299 isolated_hash = ALGO(isolated).hexdigest()
300 files = {isolated_hash:isolated}
301 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
304 'make_tree_files_read_only', 'make_tree_deleteable',
305 'make_tree_deleteable',
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'))
312 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
314 def test_run_tha_test_naked_read_only_2(self):
315 isolated = json_dumps(
317 'command': ['invalid', 'command'],
320 isolated_hash = ALGO(isolated).hexdigest()
321 files = {isolated_hash:isolated}
322 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
324 ['make_tree_read_only', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
330 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
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)
341 # Keeps tuple of (args, kwargs).
344 run_isolated.subprocess, 'call',
345 lambda *x, **y: subprocess_call.append((x, y)) or 8)
349 '--hash', isolated_hash,
350 '--cache', self.tempdir,
351 '--isolate-server', 'https://localhost',
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'))
359 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
361 def test_modified_cwd(self):
362 isolated = json_dumps({
363 'command': ['../out/some.exe', 'arg'],
364 'relative_cwd': 'some',
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'))
373 [(([self.temp_join(u'out', u'some.exe'), 'arg'],), {})],
376 def test_python_cmd(self):
377 isolated = json_dumps({
378 'command': ['../out/cmd.py', 'arg'],
379 'relative_cwd': 'some',
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.
389 [(([sys.executable, os.path.join('..', 'out', 'cmd.py'), 'arg'],), {})],
392 def test_output(self):
395 'open(sys.argv[1], "w").write("bar")\n')
396 script_hash = ALGO(script).hexdigest()
397 isolated = json_dumps(
400 'command': ['cmd.py', '${ISOLATED_OUTDIR}/foo'],
408 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
410 isolated_hash = ALGO(isolated).hexdigest()
412 isolated_hash: isolated,
416 path = os.path.join(self.tempdir, 'store')
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')
422 self.mock(sys, 'stdout', StringIO.StringIO())
423 ret = run_isolated.run_tha_test(
426 run_isolated.isolateserver.MemoryCache(),
428 self.assertEqual(0, ret)
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(
440 # TODO(maruel): Handle umask.
445 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
447 uploaded_hash = ALGO(uploaded).hexdigest()
448 hashes.add(uploaded_hash)
449 self.assertEqual(hashes, set(os.listdir(path)))
452 '[run_isolated_out_hack]',
453 '{"hash":"%s","namespace":"default-store","storage":"%s"}' % (
454 uploaded_hash, path),
455 '[/run_isolated_out_hack]'
457 self.assertEqual(expected, sys.stdout.getvalue())
459 if __name__ == '__main__':
461 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)