Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / tests / run_isolated_smoke_test.py
1 #!/usr/bin/env python
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.
5
6 import hashlib
7 import json
8 import logging
9 import os
10 import shutil
11 import subprocess
12 import sys
13 import unittest
14
15 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16 sys.path.insert(0, ROOT_DIR)
17
18 import isolated_format
19 import run_isolated
20
21 VERBOSE = False
22
23 ALGO = hashlib.sha1
24
25
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)
30     self.output = output
31     self.stderr = stderr
32     self.cwd = cwd
33
34   def __str__(self):
35     return super(CalledProcessError, self).__str__() + (
36         '\n'
37         'cwd=%s\n%s\n%s\n%s') % (
38             self.cwd,
39             self.output,
40             self.stderr,
41             ' '.join(self.cmd))
42
43
44 def list_files_tree(directory):
45   """Returns the list of all the files in a tree."""
46   actual = []
47   for root, _dirs, files in os.walk(directory):
48     actual.extend(os.path.join(root, f)[len(directory)+1:] for f in files)
49   return sorted(actual)
50
51
52 def read_content(filepath):
53   with open(filepath, 'rb') as f:
54     return f.read()
55
56
57 def write_content(filepath, content):
58   with open(filepath, 'wb') as f:
59     f.write(content)
60
61
62 def tree_modes(root):
63   """Returns the dict of files in a directory with their filemode.
64
65   Includes |root| as '.'.
66   """
67   out = {}
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)
77   return out
78
79
80 class RunIsolatedTest(unittest.TestCase):
81   def setUp(self):
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')
92     os.mkdir(self.table)
93     # The slave-side cache.
94     self.cache = os.path.join(self.tempdir, 'cache')
95
96     self.data_dir = os.path.join(ROOT_DIR, 'tests', 'run_isolated')
97
98   def tearDown(self):
99     run_isolated.rmtree(self.tempdir)
100     super(RunIsolatedTest, self).tearDown()
101
102   def _result_tree(self):
103     return list_files_tree(self.tempdir)
104
105   def _run(self, args):
106     cmd = [sys.executable, self.run_isolated_zip]
107     cmd.extend(args)
108     if VERBOSE:
109       cmd.extend(['-v'] * 2)
110       pipe = None
111     else:
112       pipe = subprocess.PIPE
113     logging.debug(' '.join(cmd))
114     proc = subprocess.Popen(
115         cmd,
116         stdout=pipe,
117         stderr=pipe,
118         universal_newlines=True,
119         cwd=self.tempdir)
120     out, err = proc.communicate()
121     return out, err, proc.returncode
122
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)
129     return result_hash
130
131   def _store(self, filename):
132     """Stores a test data file in the table.
133
134     Returns its sha-1 hash.
135     """
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))
139     return h
140
141   def _generate_args_with_isolated(self, isolated):
142     """Generates the standard arguments used with isolated as the isolated file.
143
144     Returns a list of the required arguments.
145     """
146     return [
147       '--isolated', isolated,
148       '--cache', self.cache,
149       '--indir', self.table,
150       '--namespace', 'default',
151     ]
152
153   def _generate_args_with_hash(self, hash_value):
154     """Generates the standard arguments used with |hash_value| as the hash.
155
156     Returns a list of the required arguments.
157     """
158     return [
159       '--hash', hash_value,
160       '--cache', self.cache,
161       '--indir', self.table,
162       '--namespace', 'default',
163     ]
164
165   def assertTreeModes(self, root, expected):
166     """Compares the file modes of everything in |root| with |expected|.
167
168     Arguments:
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
173                 ints.
174     """
175     actual = tree_modes(root)
176     if sys.platform == 'win32':
177       index = 2
178     elif sys.platform == 'darwin':
179       index = 1
180     else:
181       index = 0
182     expected_mangled = dict((k, oct(v[index])) for k, v in expected.iteritems())
183     self.assertEqual(expected_mangled, actual)
184
185
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')
189     expected = [
190       'state.json',
191       self._store('file1.txt'),
192       self._store('file1_copy.txt'),
193       self._store('repeated_files.py'),
194       isolated_format.hash_file(isolated, ALGO),
195     ]
196     out, err, returncode = self._run(
197         self._generate_args_with_isolated(isolated))
198     if not VERBOSE:
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)
203
204   def test_hash(self):
205     # Loads the .isolated from the store as a hash.
206     result_hash = self._store('repeated_files.isolated')
207     expected = [
208       'state.json',
209       self._store('file1.txt'),
210       self._store('file1_copy.txt'),
211       self._store('repeated_files.py'),
212       result_hash,
213     ]
214
215     out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
216     if not VERBOSE:
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)
222
223   def test_fail_empty_isolated(self):
224     result_hash = self._store_result({})
225     expected = [
226       'state.json',
227       result_hash,
228     ]
229     out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
230     if not VERBOSE:
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)
236
237   def test_includes(self):
238     # Loads an .isolated that includes another one.
239
240     # References manifest2.isolated and repeated_files.isolated. Maps file3.txt
241     # as file2.txt.
242     result_hash = self._store('check_files.isolated')
243     expected = [
244       'state.json',
245       self._store('check_files.py'),
246       self._store('file1.txt'),
247       self._store('file3.txt'),
248       # Maps file1.txt.
249       self._store('manifest1.isolated'),
250       # References manifest1.isolated. Maps file2.txt but it is overriden.
251       self._store('manifest2.isolated'),
252       result_hash,
253       self._store('repeated_files.py'),
254       self._store('repeated_files.isolated'),
255     ]
256     out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
257     if not VERBOSE:
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)
263
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')
268     expected = [
269         'state.json',
270         result_hash,
271         self._store('file1.txt'),
272         self._store('repeated_files.py')
273     ]
274
275     out, err, returncode = self._run(self._generate_args_with_hash(result_hash))
276     if not VERBOSE:
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)
282
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.
290
291     # Run the test once to generate the cache.
292     out, err, returncode = self._run(self._generate_args_with_isolated(
293         isolated_file))
294     if VERBOSE:
295       print out
296       print err
297     self.assertEqual(0, returncode)
298     expected = {
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
303       # load.
304       file1_hash: (0100400, 0100400, 0100666),
305       isolated_hash: (0100400, 0100400, 0100444),
306     }
307     self.assertTreeModes(self.cache, expected)
308
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.
318     self.assertNotEqual(
319         os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
320         os.stat(cached_file_path).st_size)
321
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(
324         isolated_file))
325     if VERBOSE:
326       print out
327       print err
328     self.assertEqual(0, returncode)
329     expected = {
330       '.': (040700, 040700, 040777),
331       'state.json': (0100600, 0100600, 0100666),
332       file1_hash: (0100400, 0100400, 0100666),
333       isolated_hash: (0100400, 0100400, 0100444),
334     }
335     self.assertTreeModes(self.cache, expected)
336
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))
340
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.
348
349     # Run the test once to generate the cache.
350     out, err, returncode = self._run(self._generate_args_with_isolated(
351         isolated_file))
352     if VERBOSE:
353       print out
354       print err
355     self.assertEqual(0, returncode)
356     expected = {
357       '.': (040775, 040755, 040777),
358       'state.json': (0100664, 0100644, 0100666),
359       file1_hash: (0100400, 0100400, 0100666),
360       isolated_hash: (0100400, 0100400, 0100444),
361     }
362     self.assertTreeModes(self.cache, expected)
363
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)
372     self.assertEqual(
373         os.stat(os.path.join(self.data_dir, 'file1.txt')).st_size,
374         os.stat(cached_file_path).st_size)
375
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(
378         isolated_file))
379     if VERBOSE:
380       print out
381       print err
382     self.assertEqual(0, returncode)
383     expected = {
384       '.': (040700, 040700, 040777),
385       'state.json': (0100600, 0100600, 0100666),
386       file1_hash: (0100400, 0100400, 0100666),
387       isolated_hash: (0100400, 0100400, 0100444),
388     }
389     self.assertTreeModes(self.cache, expected)
390
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))
396
397
398 if __name__ == '__main__':
399   VERBOSE = '-v' in sys.argv
400   logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)
401   unittest.main()