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
26 class CalledProcessError(subprocess.CalledProcessError):
27 """Makes 2.6 version act like 2.7"""
28 def __init__(self, returncode, cmd, output, stderr, cwd):
29 super(CalledProcessError, self).__init__(returncode, cmd)
35 return super(CalledProcessError, self).__str__() + (
37 'cwd=%s\n%s\n%s\n%s') % (
44 def list_files_tree(directory):
45 """Returns the list of all the files in a tree."""
47 for root, _dirs, files in os.walk(directory):
48 actual.extend(os.path.join(root, f)[len(directory)+1:] for f in files)
52 def read_content(filepath):
53 with open(filepath, 'rb') as f:
57 def write_content(filepath, content):
58 with open(filepath, 'wb') as f:
63 """Returns the dict of files in a directory with their filemode.
65 Includes |root| as '.'.
68 offset = len(root.rstrip('/\\')) + 1
69 out['.'] = oct(os.stat(root).st_mode)
70 for dirpath, dirnames, filenames in os.walk(root):
71 for filename in filenames:
72 p = os.path.join(dirpath, filename)
73 out[p[offset:]] = oct(os.stat(p).st_mode)
74 for dirname in dirnames:
75 p = os.path.join(dirpath, dirname)
76 out[p[offset:]] = oct(os.stat(p).st_mode)
80 class RunIsolatedTest(unittest.TestCase):
82 super(RunIsolatedTest, self).setUp()
83 self.tempdir = run_isolated.make_temp_dir(
84 'run_isolated_smoke_test', ROOT_DIR)
85 logging.debug(self.tempdir)
86 # run_isolated.zip executable package.
87 self.run_isolated_zip = os.path.join(self.tempdir, 'run_isolated.zip')
88 run_isolated.get_as_zip_package().zip_into_file(
89 self.run_isolated_zip, compress=False)
90 # The "source" hash table.
91 self.table = os.path.join(self.tempdir, 'table')
93 # The slave-side cache.
94 self.cache = os.path.join(self.tempdir, 'cache')
96 self.data_dir = os.path.join(ROOT_DIR, 'tests', 'run_isolated')
99 run_isolated.rmtree(self.tempdir)
100 super(RunIsolatedTest, self).tearDown()
102 def _result_tree(self):
103 return list_files_tree(self.tempdir)
105 def _run(self, args):
106 cmd = [sys.executable, self.run_isolated_zip]
109 cmd.extend(['-v'] * 2)
112 pipe = subprocess.PIPE
113 logging.debug(' '.join(cmd))
114 proc = subprocess.Popen(
118 universal_newlines=True,
120 out, err = proc.communicate()
121 return out, err, proc.returncode
123 def _store_result(self, result_data):
124 """Stores a .isolated file in the hash table."""
125 # Need to know the hash before writting the file.
126 result_text = json.dumps(result_data, sort_keys=True, indent=2)
127 result_hash = ALGO(result_text).hexdigest()
128 write_content(os.path.join(self.table, result_hash), result_text)
131 def _store(self, filename):
132 """Stores a test data file in the table.
134 Returns its sha-1 hash.
136 filepath = os.path.join(self.data_dir, filename)
137 h = isolated_format.hash_file(filepath, ALGO)
138 shutil.copyfile(filepath, os.path.join(self.table, h))
141 def _generate_args_with_isolated(self, isolated):
142 """Generates the standard arguments used with isolated as the isolated file.
144 Returns a list of the required arguments.
147 '--isolated', isolated,
148 '--cache', self.cache,
149 '--indir', self.table,
150 '--namespace', 'default',
153 def _generate_args_with_hash(self, hash_value):
154 """Generates the standard arguments used with |hash_value| as the hash.
156 Returns a list of the required arguments.
159 '--hash', hash_value,
160 '--cache', self.cache,
161 '--indir', self.table,
162 '--namespace', 'default',
165 def assertTreeModes(self, root, expected):
166 """Compares the file modes of everything in |root| with |expected|.
169 root: directory to list its tree.
170 expected: dict(relpath: (linux_mode, mac_mode, win_mode)) where each mode
171 is the expected file mode on this OS. For practical purposes,
172 linux is "anything but OSX or Windows". The modes should be
175 actual = tree_modes(root)
176 if sys.platform == 'win32':
178 elif sys.platform == 'darwin':
182 expected_mangled = dict((k, oct(v[index])) for k, v in expected.iteritems())
183 self.assertEqual(expected_mangled, actual)
186 def test_result(self):
187 # Loads an arbitrary .isolated on the file system.
188 isolated = os.path.join(self.data_dir, 'repeated_files.isolated')
191 self._store('file1.txt'),
192 self._store('file1_copy.txt'),
193 self._store('repeated_files.py'),
194 isolated_format.hash_file(isolated, ALGO),
196 out, err, returncode = self._run(
197 self._generate_args_with_isolated(isolated))
199 self.assertEqual('Success\n', out, (out, err))
200 self.assertEqual(0, returncode)
201 actual = list_files_tree(self.cache)
202 self.assertEqual(sorted(set(expected)), actual)
205 # Loads the .isolated from the store as a hash.
206 result_hash = self._store('repeated_files.isolated')
209 self._store('file1.txt'),
210 self._store('file1_copy.txt'),
211 self._store('repeated_files.py'),
215 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
217 self.assertEqual('', err)
218 self.assertEqual('Success\n', out, out)
219 self.assertEqual(0, returncode)
220 actual = list_files_tree(self.cache)
221 self.assertEqual(sorted(set(expected)), actual)
223 def test_fail_empty_isolated(self):
224 result_hash = self._store_result({})
229 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
231 self.assertEqual('', out)
232 self.assertIn('No command to run\n', err)
233 self.assertEqual(1, returncode)
234 actual = list_files_tree(self.cache)
235 self.assertEqual(sorted(expected), actual)
237 def test_includes(self):
238 # Loads an .isolated that includes another one.
240 # References manifest2.isolated and repeated_files.isolated. Maps file3.txt
242 result_hash = self._store('check_files.isolated')
245 self._store('check_files.py'),
246 self._store('file1.txt'),
247 self._store('file3.txt'),
249 self._store('manifest1.isolated'),
250 # References manifest1.isolated. Maps file2.txt but it is overriden.
251 self._store('manifest2.isolated'),
253 self._store('repeated_files.py'),
254 self._store('repeated_files.isolated'),
256 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
258 self.assertEqual('', err)
259 self.assertEqual('Success\n', out)
260 self.assertEqual(0, returncode)
261 actual = list_files_tree(self.cache)
262 self.assertEqual(sorted(expected), actual)
264 def test_link_all_hash_instances(self):
265 # Load an isolated file with the same file (same sha-1 hash), listed under
266 # two different names and ensure both are created.
267 result_hash = self._store('repeated_files.isolated')
271 self._store('file1.txt'),
272 self._store('repeated_files.py')
275 out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
277 self.assertEqual('', err)
278 self.assertEqual('Success\n', out)
279 self.assertEqual(0, returncode)
280 actual = list_files_tree(self.cache)
281 self.assertEqual(sorted(expected), actual)
283 def test_delete_quite_corrupted_cache_entry(self):
284 # Test that an entry with an invalid file size properly gets removed and
285 # fetched again. This test case also check for file modes.
286 isolated_file = os.path.join(self.data_dir, 'file_with_size.isolated')
287 isolated_hash = isolated_format.hash_file(isolated_file, ALGO)
288 file1_hash = self._store('file1.txt')
289 # Note that <tempdir>/table/<file1_hash> has 640 mode.
291 # Run the test once to generate the cache.
292 out, err, returncode = self._run(self._generate_args_with_isolated(
297 self.assertEqual(0, returncode)
299 '.': (040775, 040755, 040777),
300 'state.json': (0100664, 0100644, 0100666),
301 # The reason for 0100666 on Windows is that the file node had to be
302 # modified to delete the hardlinked node. The read only bit is reset on
304 file1_hash: (0100400, 0100400, 0100666),
305 isolated_hash: (0100400, 0100400, 0100444),
307 self.assertTreeModes(self.cache, expected)
309 # Modify one of the files in the cache to be invalid.
310 cached_file_path = os.path.join(self.cache, file1_hash)
311 previous_mode = os.stat(cached_file_path).st_mode
312 os.chmod(cached_file_path, 0600)
313 old_content = read_content(cached_file_path)
314 write_content(cached_file_path, old_content + ' but now invalid size')
315 os.chmod(cached_file_path, previous_mode)
316 logging.info('Modified %s', cached_file_path)
317 # Ensure that the cache has an invalid file.
319 os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
320 os.stat(cached_file_path).st_size)
322 # Rerun the test and make sure the cache contains the right file afterwards.
323 out, err, returncode = self._run(self._generate_args_with_isolated(
328 self.assertEqual(0, returncode)
330 '.': (040700, 040700, 040777),
331 'state.json': (0100600, 0100600, 0100666),
332 file1_hash: (0100400, 0100400, 0100666),
333 isolated_hash: (0100400, 0100400, 0100444),
335 self.assertTreeModes(self.cache, expected)
337 self.assertEqual(os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
338 os.stat(cached_file_path).st_size)
339 self.assertEqual(old_content, read_content(cached_file_path))
341 def test_delete_slightly_corrupted_cache_entry(self):
342 # Test that an entry with an invalid file size properly gets removed and
343 # fetched again. This test case also check for file modes.
344 isolated_file = os.path.join(self.data_dir, 'file_with_size.isolated')
345 isolated_hash = isolated_format.hash_file(isolated_file, ALGO)
346 file1_hash = self._store('file1.txt')
347 # Note that <tempdir>/table/<file1_hash> has 640 mode.
349 # Run the test once to generate the cache.
350 out, err, returncode = self._run(self._generate_args_with_isolated(
355 self.assertEqual(0, returncode)
357 '.': (040775, 040755, 040777),
358 'state.json': (0100664, 0100644, 0100666),
359 file1_hash: (0100400, 0100400, 0100666),
360 isolated_hash: (0100400, 0100400, 0100444),
362 self.assertTreeModes(self.cache, expected)
364 # Modify one of the files in the cache to be invalid.
365 cached_file_path = os.path.join(self.cache, file1_hash)
366 previous_mode = os.stat(cached_file_path).st_mode
367 os.chmod(cached_file_path, 0600)
368 old_content = read_content(cached_file_path)
369 write_content(cached_file_path, old_content[1:] + 'b')
370 os.chmod(cached_file_path, previous_mode)
371 logging.info('Modified %s', cached_file_path)
373 os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
374 os.stat(cached_file_path).st_size)
376 # Rerun the test and make sure the cache contains the right file afterwards.
377 out, err, returncode = self._run(self._generate_args_with_isolated(
382 self.assertEqual(0, returncode)
384 '.': (040700, 040700, 040777),
385 'state.json': (0100600, 0100600, 0100666),
386 file1_hash: (0100400, 0100400, 0100666),
387 isolated_hash: (0100400, 0100400, 0100444),
389 self.assertTreeModes(self.cache, expected)
391 self.assertEqual(os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
392 os.stat(cached_file_path).st_size)
393 # TODO(maruel): This corruption is NOT detected.
394 # This needs to be fixed.
395 self.assertNotEqual(old_content, read_content(cached_file_path))
398 if __name__ == '__main__':
399 VERBOSE = '-v' in sys.argv
400 logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)