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.
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'))
23 from depot_tools import auto_stub
28 def write_content(filepath, content):
29 with open(filepath, 'wb') as f:
34 return json.dumps(data, sort_keys=True, separators=(',', ':'))
37 class StorageFake(object):
38 def __init__(self, files):
39 self._files = files.copy()
41 def __enter__(self, *_):
44 def __exit__(self, *_):
47 def async_fetch(self, channel, _priority, digest, _size, sink):
48 sink([self._files[digest]])
49 channel.send_result(digest)
52 class RunIsolatedTest(auto_stub.TestCase):
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)
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()
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
75 (filepath, oct(expected), oct(actual), oct(umask)))
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)
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')
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
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)
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):
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.
133 with self.assertRaises(OSError):
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
151 def test_hard_link_mode(self):
152 # Creates a hard link, see if the file mode changed on the node or the
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
171 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
174 # pylint: disable=W0613
175 def call(command, cwd, env):
176 calls.append(command)
178 self.mock(run_isolated.subprocess, 'call', call)
179 isolated = json_dumps(
181 'command': ['foo.exe', 'cmd with space'],
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)
190 '--hash', isolated_hash,
191 '--cache', self.tempdir,
192 '--isolate-server', 'https://localhost',
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)
198 def test_main_args(self):
199 self.mock(run_isolated.tools, 'disable_buffering', lambda: None)
202 # pylint: disable=W0613
203 def call(command, cwd, env):
204 calls.append(command)
206 self.mock(run_isolated.subprocess, 'call', call)
207 isolated = json_dumps(
209 'command': ['foo.exe', 'cmd with space'],
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)
218 '--hash', isolated_hash,
219 '--cache', self.tempdir,
220 '--isolate-server', 'https://localhost',
225 ret = run_isolated.main(cmd)
226 self.assertEqual(0, ret)
228 [[self.temp_join(u'foo.exe'), u'cmd with space', '--extraargs', 'bar']],
231 def _run_tha_test(self, isolated_hash, files):
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))
239 # Keeps tuple of (args, kwargs).
242 run_isolated.subprocess, 'call',
243 lambda *x, **y: subprocess_call.append((x, y)) or 0)
245 ret = run_isolated.run_tha_test(
248 run_isolated.isolateserver.MemoryCache(),
249 run_isolated.isolateserver.get_hash_algo('default-deflate'),
251 self.assertEqual(0, ret)
252 return subprocess_call, make_tree_call
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)
260 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
266 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
268 def test_run_tha_test_naked_read_only_0(self):
269 isolated = json_dumps(
271 'command': ['invalid', 'command'],
274 isolated_hash = ALGO(isolated).hexdigest()
275 files = {isolated_hash:isolated}
276 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
278 ['make_tree_writeable', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
284 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
286 def test_run_tha_test_naked_read_only_1(self):
287 isolated = json_dumps(
289 'command': ['invalid', 'command'],
292 isolated_hash = ALGO(isolated).hexdigest()
293 files = {isolated_hash:isolated}
294 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
297 'make_tree_files_read_only', 'make_tree_deleteable',
298 'make_tree_deleteable',
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'))
305 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
307 def test_run_tha_test_naked_read_only_2(self):
308 isolated = json_dumps(
310 'command': ['invalid', 'command'],
313 isolated_hash = ALGO(isolated).hexdigest()
314 files = {isolated_hash:isolated}
315 subprocess_call, make_tree_call = self._run_tha_test(isolated_hash, files)
317 ['make_tree_read_only', 'make_tree_deleteable', 'make_tree_deleteable'],
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'))
323 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
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)
334 # Keeps tuple of (args, kwargs).
337 run_isolated.subprocess, 'call',
338 lambda *x, **y: subprocess_call.append((x, y)) or 8)
342 '--hash', isolated_hash,
343 '--cache', self.tempdir,
344 '--isolate-server', 'https://localhost',
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'))
352 [(([self.temp_join(u'invalid'), u'command'],), {})], subprocess_call)
354 def test_modified_cwd(self):
355 isolated = json_dumps({
356 'command': ['../out/some.exe', 'arg'],
357 'relative_cwd': 'some',
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'))
366 [(([self.temp_join(u'out', u'some.exe'), 'arg'],), {})],
369 def test_python_cmd(self):
370 isolated = json_dumps({
371 'command': ['../out/cmd.py', 'arg'],
372 'relative_cwd': 'some',
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.
382 [(([sys.executable, os.path.join('..', 'out', 'cmd.py'), 'arg'],), {})],
385 def test_output(self):
388 'open(sys.argv[1], "w").write("bar")\n')
389 script_hash = ALGO(script).hexdigest()
390 isolated = json_dumps(
393 'command': ['cmd.py', '${ISOLATED_OUTDIR}/foo'],
401 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
403 isolated_hash = ALGO(isolated).hexdigest()
405 isolated_hash: isolated,
409 path = os.path.join(self.tempdir, 'store')
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')
415 self.mock(sys, 'stdout', StringIO.StringIO())
416 ret = run_isolated.run_tha_test(
419 run_isolated.isolateserver.MemoryCache(),
420 run_isolated.isolateserver.get_hash_algo('default-store'),
422 self.assertEqual(0, ret)
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(
434 # TODO(maruel): Handle umask.
439 'version': run_isolated.isolateserver.ISOLATED_FILE_VERSION,
441 uploaded_hash = ALGO(uploaded).hexdigest()
442 hashes.add(uploaded_hash)
443 self.assertEqual(hashes, set(os.listdir(path)))
445 'run_isolated output: %s\n' % uploaded_hash, sys.stdout.getvalue())
448 if __name__ == '__main__':
450 level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)