2 # Copyright 2012 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.
15 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16 sys.path.insert(0, ROOT_DIR)
18 import isolated_format
20 from utils import file_path
27 class CalledProcessError(subprocess.CalledProcessError):
28 """Makes 2.6 version act like 2.7"""
29 def __init__(self, returncode, cmd, output, stderr, cwd):
30 super(CalledProcessError, self).__init__(returncode, cmd)
36 return super(CalledProcessError, self).__str__() + (
38 'cwd=%s\n%s\n%s\n%s') % (
45 def list_files_tree(directory):
46 """Returns the list of all the files in a tree."""
48 for root, _dirs, files in os.walk(directory):
49 actual.extend(os.path.join(root, f)[len(directory)+1:] for f in files)
53 def read_content(filepath):
54 with open(filepath, 'rb') as f:
58 def write_content(filepath, content):
59 with open(filepath, 'wb') as f:
64 """Returns the dict of files in a directory with their filemode.
66 Includes |root| as '.'.
69 offset = len(root.rstrip('/\\')) + 1
70 out['.'] = oct(os.stat(root).st_mode)
71 for dirpath, dirnames, filenames in os.walk(root):
72 for filename in filenames:
73 p = os.path.join(dirpath, filename)
74 out[p[offset:]] = oct(os.stat(p).st_mode)
75 for dirname in dirnames:
76 p = os.path.join(dirpath, dirname)
77 out[p[offset:]] = oct(os.stat(p).st_mode)
81 class RunIsolatedTest(unittest.TestCase):
83 super(RunIsolatedTest, self).setUp()
84 self.tempdir = run_isolated.make_temp_dir(
85 'run_isolated_smoke_test', ROOT_DIR)
86 logging.debug(self.tempdir)
87 # run_isolated.zip executable package.
88 self.run_isolated_zip = os.path.join(self.tempdir, 'run_isolated.zip')
89 run_isolated.get_as_zip_package().zip_into_file(
90 self.run_isolated_zip, compress=False)
91 # The "source" hash table.
92 self.table = os.path.join(self.tempdir, 'table')
94 # The slave-side cache.
95 self.cache = os.path.join(self.tempdir, 'cache')
97 self.data_dir = os.path.join(ROOT_DIR, 'tests', 'run_isolated')
100 file_path.rmtree(self.tempdir)
101 super(RunIsolatedTest, self).tearDown()
103 def _result_tree(self):
104 return list_files_tree(self.tempdir)
106 def _run(self, args):
107 cmd = [sys.executable, self.run_isolated_zip]
109 pipe = subprocess.PIPE
110 logging.debug(' '.join(cmd))
111 proc = subprocess.Popen(
115 universal_newlines=True,
117 out, err = proc.communicate()
118 return out, err, proc.returncode
120 def _store_result(self, result_data):
121 """Stores a .isolated file in the hash table."""
122 # Need to know the hash before writting the file.
123 result_text = json.dumps(result_data, sort_keys=True, indent=2)
124 result_hash = ALGO(result_text).hexdigest()
125 write_content(os.path.join(self.table, result_hash), result_text)
128 def _store(self, filename):
129 """Stores a test data file in the table.
131 Returns its sha-1 hash.
133 filepath = os.path.join(self.data_dir, filename)
134 h = isolated_format.hash_file(filepath, ALGO)
135 shutil.copyfile(filepath, os.path.join(self.table, h))
138 def _generate_args_with_isolated(self, isolated):
139 """Generates the standard arguments used with isolated as the isolated file.
141 Returns a list of the required arguments.
144 '--isolated', isolated,
145 '--cache', self.cache,
146 '--indir', self.table,
147 '--namespace', 'default',
150 def _generate_args_with_hash(self, hash_value):
151 """Generates the standard arguments used with |hash_value| as the hash.
153 Returns a list of the required arguments.
156 '--hash', hash_value,
157 '--cache', self.cache,
158 '--indir', self.table,
159 '--namespace', 'default',
162 def assertTreeModes(self, root, expected):
163 """Compares the file modes of everything in |root| with |expected|.
166 root: directory to list its tree.
167 expected: dict(relpath: (linux_mode, mac_mode, win_mode)) where each mode
168 is the expected file mode on this OS. For practical purposes,
169 linux is "anything but OSX or Windows". The modes should be
172 actual = tree_modes(root)
173 if sys.platform == 'win32':
175 elif sys.platform == 'darwin':
179 expected_mangled = dict((k, oct(v[index])) for k, v in expected.iteritems())
180 self.assertEqual(expected_mangled, actual)
183 def test_result(self):
184 # Loads an arbitrary .isolated on the file system.
185 isolated = os.path.join(self.data_dir, 'repeated_files.isolated')
188 self._store('file1.txt'),
189 self._store('file1_copy.txt'),
190 self._store('repeated_files.py'),
191 isolated_format.hash_file(isolated, ALGO),
193 out, err, returncode = self._run(
194 self._generate_args_with_isolated(isolated))
195 self.assertEqual('Success\n', out, (out, err))
196 self.assertEqual(0, returncode)
197 actual = list_files_tree(self.cache)
198 self.assertEqual(sorted(set(expected)), actual)
201 # Loads the .isolated from the store as a hash.
202 result_hash = self._store('repeated_files.isolated')
205 self._store('file1.txt'),
206 self._store('file1_copy.txt'),
207 self._store('repeated_files.py'),
211 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
212 self.assertEqual('', err)
213 self.assertEqual('Success\n', out, out)
214 self.assertEqual(0, returncode)
215 actual = list_files_tree(self.cache)
216 self.assertEqual(sorted(set(expected)), actual)
218 def test_fail_empty_isolated(self):
219 result_hash = self._store_result({})
224 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
225 self.assertEqual('', out)
226 self.assertIn('No command to run\n', err)
227 self.assertEqual(1, returncode)
228 actual = list_files_tree(self.cache)
229 self.assertEqual(sorted(expected), actual)
231 def test_includes(self):
232 # Loads an .isolated that includes another one.
234 # References manifest2.isolated and repeated_files.isolated. Maps file3.txt
236 result_hash = self._store('check_files.isolated')
239 self._store('check_files.py'),
240 self._store('file1.txt'),
241 self._store('file3.txt'),
243 self._store('manifest1.isolated'),
244 # References manifest1.isolated. Maps file2.txt but it is overriden.
245 self._store('manifest2.isolated'),
247 self._store('repeated_files.py'),
248 self._store('repeated_files.isolated'),
250 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
251 self.assertEqual('', err)
252 self.assertEqual('Success\n', out)
253 self.assertEqual(0, returncode)
254 actual = list_files_tree(self.cache)
255 self.assertEqual(sorted(expected), actual)
257 def test_link_all_hash_instances(self):
258 # Load an isolated file with the same file (same sha-1 hash), listed under
259 # two different names and ensure both are created.
260 result_hash = self._store('repeated_files.isolated')
264 self._store('file1.txt'),
265 self._store('repeated_files.py')
268 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
269 self.assertEqual('', err)
270 self.assertEqual('Success\n', out)
271 self.assertEqual(0, returncode)
272 actual = list_files_tree(self.cache)
273 self.assertEqual(sorted(expected), actual)
275 def test_delete_quite_corrupted_cache_entry(self):
276 # Test that an entry with an invalid file size properly gets removed and
277 # fetched again. This test case also check for file modes.
278 isolated_file = os.path.join(self.data_dir, 'file_with_size.isolated')
279 isolated_hash = isolated_format.hash_file(isolated_file, ALGO)
280 file1_hash = self._store('file1.txt')
281 # Note that <tempdir>/table/<file1_hash> has 640 mode.
283 # Run the test once to generate the cache.
284 _out, _err, returncode = self._run(self._generate_args_with_isolated(
286 self.assertEqual(0, returncode)
288 '.': (040707, 040707, 040777),
289 'state.json': (0100606, 0100606, 0100666),
290 # The reason for 0100666 on Windows is that the file node had to be
291 # modified to delete the hardlinked node. The read only bit is reset on
293 file1_hash: (0100400, 0100400, 0100666),
294 isolated_hash: (0100400, 0100400, 0100444),
296 self.assertTreeModes(self.cache, expected)
298 # Modify one of the files in the cache to be invalid.
299 cached_file_path = os.path.join(self.cache, file1_hash)
300 previous_mode = os.stat(cached_file_path).st_mode
301 os.chmod(cached_file_path, 0600)
302 old_content = read_content(cached_file_path)
303 write_content(cached_file_path, old_content + ' but now invalid size')
304 os.chmod(cached_file_path, previous_mode)
305 logging.info('Modified %s', cached_file_path)
306 # Ensure that the cache has an invalid file.
308 os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
309 os.stat(cached_file_path).st_size)
311 # Rerun the test and make sure the cache contains the right file afterwards.
312 _out, _err, returncode = self._run(self._generate_args_with_isolated(
314 self.assertEqual(0, returncode)
316 '.': (040700, 040700, 040777),
317 'state.json': (0100600, 0100600, 0100666),
318 file1_hash: (0100400, 0100400, 0100666),
319 isolated_hash: (0100400, 0100400, 0100444),
321 self.assertTreeModes(self.cache, expected)
323 self.assertEqual(os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
324 os.stat(cached_file_path).st_size)
325 self.assertEqual(old_content, read_content(cached_file_path))
327 def test_delete_slightly_corrupted_cache_entry(self):
328 # Test that an entry with an invalid file size properly gets removed and
329 # fetched again. This test case also check for file modes.
330 isolated_file = os.path.join(self.data_dir, 'file_with_size.isolated')
331 isolated_hash = isolated_format.hash_file(isolated_file, ALGO)
332 file1_hash = self._store('file1.txt')
333 # Note that <tempdir>/table/<file1_hash> has 640 mode.
335 # Run the test once to generate the cache.
336 _out, _err, returncode = self._run(self._generate_args_with_isolated(
338 self.assertEqual(0, returncode)
340 '.': (040707, 040707, 040777),
341 'state.json': (0100606, 0100606, 0100666),
342 file1_hash: (0100400, 0100400, 0100666),
343 isolated_hash: (0100400, 0100400, 0100444),
345 self.assertTreeModes(self.cache, expected)
347 # Modify one of the files in the cache to be invalid.
348 cached_file_path = os.path.join(self.cache, file1_hash)
349 previous_mode = os.stat(cached_file_path).st_mode
350 os.chmod(cached_file_path, 0600)
351 old_content = read_content(cached_file_path)
352 write_content(cached_file_path, old_content[1:] + 'b')
353 os.chmod(cached_file_path, previous_mode)
354 logging.info('Modified %s', cached_file_path)
356 os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
357 os.stat(cached_file_path).st_size)
359 # Rerun the test and make sure the cache contains the right file afterwards.
360 _out, _err, returncode = self._run(self._generate_args_with_isolated(
362 self.assertEqual(0, returncode)
364 '.': (040700, 040700, 040777),
365 'state.json': (0100600, 0100600, 0100666),
366 file1_hash: (0100400, 0100400, 0100666),
367 isolated_hash: (0100400, 0100400, 0100444),
369 self.assertTreeModes(self.cache, expected)
371 self.assertEqual(os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
372 os.stat(cached_file_path).st_size)
373 # TODO(maruel): This corruption is NOT detected.
374 # This needs to be fixed.
375 self.assertNotEqual(old_content, read_content(cached_file_path))
378 if __name__ == '__main__':