Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / tests / isolate_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 cStringIO
7 import hashlib
8 import json
9 import logging
10 import os
11 import re
12 import shutil
13 import stat
14 import subprocess
15 import sys
16 import tempfile
17 import unittest
18
19 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 sys.path.insert(0, ROOT_DIR)
21
22 import isolate
23 import isolateserver
24 from utils import file_path
25 import trace_test_util
26 # Create shortcuts.
27 from isolate_format import KEY_TOUCHED, KEY_TRACKED, KEY_UNTRACKED
28
29
30 VERBOSE = False
31
32 ALGO = hashlib.sha1
33 HASH_NULL = ALGO().hexdigest()
34
35
36 # These are per test case, not per mode.
37 RELATIVE_CWD = {
38   'all_items_invalid': '.',
39   'fail': '.',
40   'missing_trailing_slash': '.',
41   'no_run': '.',
42   'non_existent': '.',
43   'split': '.',
44   'symlink_full': '.',
45   'symlink_partial': '.',
46   'symlink_outside_build_root': '.',
47   'touch_only': '.',
48   'touch_root': os.path.join('tests', 'isolate'),
49   'with_flag': '.',
50 }
51
52 DEPENDENCIES = {
53   'all_items_invalid' : ['empty.py'],
54   'fail': ['fail.py'],
55   'missing_trailing_slash': [],
56   'no_run': [
57     'no_run.isolate',
58     os.path.join('files1', 'subdir', '42.txt'),
59     os.path.join('files1', 'test_file1.txt'),
60     os.path.join('files1', 'test_file2.txt'),
61   ],
62   'non_existent': [],
63   'split': [
64     os.path.join('files1', 'subdir', '42.txt'),
65     os.path.join('test', 'data', 'foo.txt'),
66     'split.py',
67   ],
68   'symlink_full': [
69     os.path.join('files1', 'subdir', '42.txt'),
70     os.path.join('files1', 'test_file1.txt'),
71     os.path.join('files1', 'test_file2.txt'),
72     # files2 is a symlink to files1.
73     'files2',
74     'symlink_full.py',
75   ],
76   'symlink_partial': [
77     os.path.join('files1', 'test_file2.txt'),
78     # files2 is a symlink to files1.
79     'files2',
80     'symlink_partial.py',
81   ],
82   'symlink_outside_build_root': [
83     os.path.join('link_outside_build_root', 'test_file3.txt'),
84     'symlink_outside_build_root.py',
85   ],
86   'touch_only': [
87     'touch_only.py',
88     os.path.join('files1', 'test_file1.txt'),
89   ],
90   'touch_root': [
91     os.path.join('tests', 'isolate', 'touch_root.py'),
92     'isolate.py',
93   ],
94   'with_flag': [
95     'with_flag.py',
96     os.path.join('files1', 'subdir', '42.txt'),
97     os.path.join('files1', 'test_file1.txt'),
98     os.path.join('files1', 'test_file2.txt'),
99   ],
100 }
101
102
103 class CalledProcessError(subprocess.CalledProcessError):
104   """Makes 2.6 version act like 2.7"""
105   def __init__(self, returncode, cmd, output, stderr, cwd):
106     super(CalledProcessError, self).__init__(returncode, cmd)
107     self.output = output
108     self.stderr = stderr
109     self.cwd = cwd
110
111   def __str__(self):
112     return super(CalledProcessError, self).__str__() + (
113         '\n'
114         'cwd=%s\n%s\n%s\n%s') % (
115             self.cwd,
116             self.output,
117             self.stderr,
118             ' '.join(self.cmd))
119
120
121 def list_files_tree(directory):
122   """Returns the list of all the files in a tree."""
123   actual = []
124   for root, dirnames, filenames in os.walk(directory):
125     actual.extend(os.path.join(root, f)[len(directory)+1:] for f in filenames)
126     for dirname in dirnames:
127       full = os.path.join(root, dirname)
128       # Manually include symlinks.
129       if os.path.islink(full):
130         actual.append(full[len(directory)+1:])
131   return sorted(actual)
132
133
134 def _isolate_dict_to_string(values):
135   buf = cStringIO.StringIO()
136   isolate.isolate_format.pretty_print(values, buf)
137   return buf.getvalue()
138
139
140 def _wrap_in_condition(variables):
141   """Wraps a variables dict inside the current OS condition.
142
143   Returns the equivalent string.
144   """
145   return _isolate_dict_to_string(
146       {
147         'conditions': [
148           ['OS=="mac" and chromeos==0', {
149             'variables': variables
150           }],
151         ],
152       })
153
154
155 def _fix_file_mode(filename, read_only):
156   """4 modes are supported, 0750 (rwx), 0640 (rw), 0550 (rx), 0440 (r)."""
157   min_mode = 0440
158   if not read_only:
159     min_mode |= 0200
160   return (min_mode | 0110) if filename.endswith('.py') else min_mode
161
162
163 class Isolate(unittest.TestCase):
164   def test_help_modes(self):
165     # Check coherency in the help and implemented modes.
166     p = subprocess.Popen(
167         [sys.executable, os.path.join(ROOT_DIR, 'isolate.py'), '--help'],
168         stdout=subprocess.PIPE,
169         stderr=subprocess.STDOUT,
170         cwd=ROOT_DIR)
171     out = p.communicate()[0].splitlines()
172     self.assertEqual(0, p.returncode)
173     out = out[out.index('Commands are:') + 1:]
174     out = out[:out.index('')]
175     regexp = '^  (?:\x1b\\[\\d\\dm|)(\\w+)\s*(:?\x1b\\[\\d\\dm|) .+'
176     modes = [re.match(regexp, l) for l in out]
177     modes = [m.group(1) for m in modes if m]
178     EXPECTED_MODES = (
179         'archive',
180         'check',
181         'hashtable',
182         'help',
183         'merge',
184         'read',
185         'remap',
186         'rewrite',
187         'run',
188         'trace',
189     )
190     # If a new command is added it should at least has a bare test.
191     self.assertEqual(sorted(EXPECTED_MODES), sorted(modes))
192
193
194 class IsolateTempdir(unittest.TestCase):
195   def setUp(self):
196     super(IsolateTempdir, self).setUp()
197     self.tempdir = tempfile.mkdtemp(prefix='isolate_smoke_')
198     self.isolated = os.path.join(self.tempdir, 'isolate_smoke_test.isolated')
199
200   def tearDown(self):
201     try:
202       logging.debug(self.tempdir)
203       shutil.rmtree(self.tempdir)
204     finally:
205       super(IsolateTempdir, self).tearDown()
206
207   def _gen_files(self, read_only, empty_file, with_time):
208     """Returns a dict of files like calling isolate.process_input() on each
209     file.
210
211     Arguments:
212     - read_only: Mark all the 'm' modes without the writeable bit.
213     - empty_file: Add a specific empty file (size 0).
214     - with_time: Include 't' timestamps. For saved state .state files.
215     """
216     root_dir = ROOT_DIR
217     if RELATIVE_CWD[self.case()] == '.':
218       root_dir = os.path.join(root_dir, 'tests', 'isolate')
219
220     files = dict((unicode(f), {}) for f in DEPENDENCIES[self.case()])
221
222     for relfile, v in files.iteritems():
223       filepath = os.path.join(root_dir, relfile)
224       filestats = os.lstat(filepath)
225       is_link = stat.S_ISLNK(filestats.st_mode)
226       if not is_link:
227         v[u's'] = filestats.st_size
228         if sys.platform != 'win32':
229           v[u'm'] = _fix_file_mode(relfile, read_only)
230       if with_time:
231         # Used to skip recalculating the hash. Use the most recent update
232         # time.
233         v[u't'] = int(round(filestats.st_mtime))
234       if is_link:
235         v[u'l'] = os.readlink(filepath)  # pylint: disable=E1101
236       else:
237         # Upgrade the value to unicode so diffing the structure in case of
238         # test failure is easier, since the basestring type must match,
239         # str!=unicode.
240         v[u'h'] = unicode(isolateserver.hash_file(filepath, ALGO))
241
242     if empty_file:
243       item = files[empty_file]
244       item['h'] = unicode(HASH_NULL)
245       if sys.platform != 'win32':
246         item['m'] = 288
247       item['s'] = 0
248       if with_time:
249         item['T'] = True
250         item.pop('t', None)
251     return files
252
253   def _expected_isolated(self, args, read_only, empty_file):
254     """Verifies self.isolated contains the expected data."""
255     expected = {
256       u'algo': u'sha-1',
257       u'files': self._gen_files(read_only, empty_file, False),
258       u'relative_cwd': unicode(RELATIVE_CWD[self.case()]),
259       u'version': unicode(isolate.isolateserver.ISOLATED_FILE_VERSION),
260     }
261     if read_only is not None:
262       expected[u'read_only'] = read_only
263     if args:
264       expected[u'command'] = [u'python'] + [unicode(x) for x in args]
265     self.assertEqual(expected, json.load(open(self.isolated, 'r')))
266
267   def _expected_saved_state(
268       self, args, read_only, empty_file, extra_vars, root_dir):
269     expected = {
270       u'OS': unicode(sys.platform),
271       u'algo': u'sha-1',
272       u'child_isolated_files': [],
273       u'command': [],
274       u'config_variables': {
275         u'OS': u'mac',
276         u'chromeos': 0,
277       },
278       u'extra_variables': {
279         u'EXECUTABLE_SUFFIX': u'.exe' if sys.platform == 'win32' else u'',
280       },
281       u'files': self._gen_files(read_only, empty_file, True),
282       u'isolate_file': file_path.safe_relpath(
283           file_path.get_native_path_case(unicode(self.filename())),
284           unicode(os.path.dirname(self.isolated))),
285       u'path_variables': {},
286       u'relative_cwd': unicode(RELATIVE_CWD[self.case()]),
287       u'root_dir': unicode(root_dir or os.path.dirname(self.filename())),
288       u'version': unicode(isolate.SavedState.EXPECTED_VERSION),
289     }
290     if args:
291       expected[u'command'] = [u'python'] + [unicode(x) for x in args]
292     expected['extra_variables'].update(extra_vars or {})
293     self.assertEqual(expected, json.load(open(self.saved_state(), 'r')))
294
295   def _expect_results(
296       self, args, read_only, extra_vars, empty_file, root_dir=None):
297     self._expected_isolated(args, read_only, empty_file)
298     self._expected_saved_state(
299         args, read_only, empty_file, extra_vars, root_dir)
300     # Also verifies run_isolated.py will be able to read it.
301     with open(self.isolated, 'rb') as f:
302       isolate.isolateserver.load_isolated(f.read(), ALGO)
303
304   def _expect_no_result(self):
305     self.assertFalse(os.path.exists(self.isolated))
306
307   def _get_cmd(self, mode):
308     return [
309       sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
310       mode,
311       '--isolated', self.isolated,
312       '--isolate', self.filename(),
313       '--config-variable', 'OS', 'mac',
314       '--config-variable', 'chromeos', '0',
315     ]
316
317   def _execute(self, mode, case, args, need_output, cwd=ROOT_DIR):
318     """Executes isolate.py."""
319     self.assertEqual(
320         case,
321         self.case() + '.isolate',
322         'Rename the test case to test_%s()' % case)
323     cmd = self._get_cmd(mode)
324     cmd.extend(args)
325
326     env = os.environ.copy()
327     if 'ISOLATE_DEBUG' in env:
328       del env['ISOLATE_DEBUG']
329
330     if need_output or not VERBOSE:
331       stdout = subprocess.PIPE
332       stderr = subprocess.PIPE
333     else:
334       cmd.extend(['-v'] * 3)
335       stdout = None
336       stderr = None
337
338     logging.debug(cmd)
339     p = subprocess.Popen(
340         cmd,
341         stdout=stdout,
342         stderr=stderr,
343         cwd=cwd,
344         env=env,
345         universal_newlines=True)
346     out, err = p.communicate()
347     if p.returncode:
348       raise CalledProcessError(p.returncode, cmd, out, err, cwd)
349
350     # Do not check on Windows since a lot of spew is generated there.
351     if sys.platform != 'win32':
352       self.assertTrue(err in (None, ''), err)
353     return out
354
355   def case(self):
356     """Returns the filename corresponding to this test case."""
357     test_id = self.id().split('.')
358     return re.match('^test_([a-z_]+)$', test_id[2]).group(1)
359
360   def filename(self):
361     """Returns the filename corresponding to this test case."""
362     filename = os.path.join(
363         ROOT_DIR, 'tests', 'isolate', self.case() + '.isolate')
364     self.assertTrue(os.path.isfile(filename), filename)
365     return filename
366
367   def saved_state(self):
368     return isolate.isolatedfile_to_state(self.isolated)
369
370   def _test_all_items_invalid(self, mode):
371     out = self._execute(mode, 'all_items_invalid.isolate',
372                         ['--ignore_broken_item'], True)
373     self._expect_results(['empty.py'], None, None, None)
374
375     return out or ''
376
377   def _test_missing_trailing_slash(self, mode):
378     try:
379       self._execute(mode, 'missing_trailing_slash.isolate', [], True)
380       self.fail()
381     except subprocess.CalledProcessError as e:
382       self.assertEqual('', e.output)
383       out = e.stderr
384     self._expect_no_result()
385     root = file_path.get_native_path_case(unicode(ROOT_DIR))
386     expected = (
387       'Input directory %s must have a trailing slash' %
388           os.path.join(root, 'tests', 'isolate', 'files1')
389     )
390     self.assertIn(expected, out)
391
392   def _test_non_existent(self, mode):
393     try:
394       self._execute(mode, 'non_existent.isolate', [], True)
395       self.fail()
396     except subprocess.CalledProcessError as e:
397       self.assertEqual('', e.output)
398       out = e.stderr
399     self._expect_no_result()
400     root = file_path.get_native_path_case(unicode(ROOT_DIR))
401     expected = (
402       'Input file %s doesn\'t exist' %
403           os.path.join(root, 'tests', 'isolate', 'A_file_that_do_not_exist')
404     )
405     self.assertIn(expected, out)
406
407
408 class IsolateOutdir(IsolateTempdir):
409   def setUp(self):
410     super(IsolateOutdir, self).setUp()
411     # The tests assume the current directory is the file's directory.
412     os.chdir(ROOT_DIR)
413     self.outdir = os.path.join(self.tempdir, 'isolated')
414
415   def _expect_no_tree(self):
416     # No outdir was created.
417     self.assertFalse(os.path.exists(self.outdir))
418
419   def _result_tree(self):
420     return list_files_tree(self.outdir)
421
422   def _expected_tree(self):
423     """Verifies the files written in the temporary directory."""
424     self.assertEqual(sorted(DEPENDENCIES[self.case()]), self._result_tree())
425
426   def _get_cmd(self, mode):
427     """Adds --outdir for the commands supporting it."""
428     cmd = super(IsolateOutdir, self)._get_cmd(mode)
429     cmd.extend(('--outdir', self.outdir))
430     return cmd
431
432   def _test_missing_trailing_slash(self, mode):
433     super(IsolateOutdir, self)._test_missing_trailing_slash(mode)
434     self._expect_no_tree()
435
436   def _test_non_existent(self, mode):
437     super(IsolateOutdir, self)._test_non_existent(mode)
438     self._expect_no_tree()
439
440
441 class Isolate_check(IsolateTempdir):
442   def test_fail(self):
443     self._execute('check', 'fail.isolate', [], False)
444     self._expect_results(['fail.py'], None, None, None)
445
446   def test_missing_trailing_slash(self):
447     self._test_missing_trailing_slash('check')
448
449   def test_non_existent(self):
450     self._test_non_existent('check')
451
452   def test_all_items_invalid(self):
453     out = self._test_all_items_invalid('check')
454     self.assertEqual('', out)
455
456   def test_no_run(self):
457     self._execute('check', 'no_run.isolate', [], False)
458     self._expect_results([], None, None, None)
459
460   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
461   def do_not_test_touch_only(self):
462     self._execute(
463         'check', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
464         False)
465     empty = os.path.join('files1', 'test_file1.txt')
466     self._expected_isolated(['touch_only.py', 'gyp'], None, empty)
467
468   def test_touch_root(self):
469     self._execute('check', 'touch_root.isolate', [], False)
470     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
471
472   def test_with_flag(self):
473     self._execute(
474         'check', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
475         False)
476     self._expect_results(
477         ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
478
479   if sys.platform != 'win32':
480     def test_symlink_full(self):
481       self._execute('check', 'symlink_full.isolate', [], False)
482       self._expect_results(['symlink_full.py'], None, None, None)
483
484     def test_symlink_partial(self):
485       self._execute('check', 'symlink_partial.isolate', [], False)
486       self._expect_results(['symlink_partial.py'], None, None, None)
487
488     def test_symlink_outside_build_root(self):
489       self._execute('check', 'symlink_outside_build_root.isolate', [], False)
490       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
491
492
493 class Isolate_hashtable(IsolateOutdir):
494   def _gen_expected_tree(self, empty_file):
495     expected = [
496       unicode(v['h'])
497       for v in self._gen_files(False, empty_file, False).itervalues()
498     ]
499     expected.append(
500         unicode(isolateserver.hash_file(self.isolated, ALGO)))
501     return expected
502
503   def _expected_hash_tree(self, empty_file):
504     """Verifies the files written in the temporary directory."""
505     self.assertEqual(
506         sorted(self._gen_expected_tree(empty_file)),
507         map(unicode, self._result_tree()))
508
509   def test_fail(self):
510     self._execute('hashtable', 'fail.isolate', [], False)
511     self._expected_hash_tree(None)
512     self._expect_results(['fail.py'], None, None, None)
513
514   def test_missing_trailing_slash(self):
515     self._test_missing_trailing_slash('hashtable')
516
517   def test_non_existent(self):
518     self._test_non_existent('hashtable')
519
520   def test_all_items_invalid(self):
521     out = self._test_all_items_invalid('hashtable')
522     expected = (
523         '%s  isolate_smoke_test.isolated\n' %
524         isolateserver.hash_file(self.isolated, ALGO))
525     self.assertEqual(expected, out)
526     self._expected_hash_tree(None)
527
528   def test_no_run(self):
529     self._execute('hashtable', 'no_run.isolate', [], False)
530     self._expected_hash_tree(None)
531     self._expect_results([], None, None, None)
532
533   def test_split(self):
534     self._execute(
535         'hashtable',
536         'split.isolate',
537         [
538           '--path-variable', 'DEPTH', '.',
539           '--path-variable', 'PRODUCT_DIR', 'files1',
540         ],
541         False,
542         cwd=os.path.join(ROOT_DIR, 'tests', 'isolate'))
543     # Reimplement _expected_hash_tree():
544     tree = self._gen_expected_tree(None)
545     isolated_base = self.isolated[:-len('.isolated')]
546     isolated_hashes = [
547       unicode(isolateserver.hash_file(isolated_base + '.0.isolated', ALGO)),
548       unicode(isolateserver.hash_file(isolated_base + '.1.isolated', ALGO)),
549     ]
550     tree.extend(isolated_hashes)
551     self.assertEqual(sorted(tree), map(unicode, self._result_tree()))
552
553     # Reimplement _expected_isolated():
554     files = self._gen_files(None, None, False)
555     expected = {
556       u'algo': u'sha-1',
557       u'command': [u'python', u'split.py'],
558       u'files': {u'split.py': files['split.py']},
559       u'includes': isolated_hashes,
560       u'relative_cwd': unicode(RELATIVE_CWD[self.case()]),
561       u'version': unicode(isolate.isolateserver.ISOLATED_FILE_VERSION),
562     }
563     self.assertEqual(expected, json.load(open(self.isolated, 'r')))
564
565     key = os.path.join(u'test', 'data', 'foo.txt')
566     expected = {
567       u'algo': u'sha-1',
568       u'files': {key: files[key]},
569       u'version': unicode(isolate.isolateserver.ISOLATED_FILE_VERSION),
570     }
571     self.assertEqual(
572         expected, json.load(open(isolated_base + '.0.isolated', 'r')))
573
574     key = os.path.join(u'files1', 'subdir', '42.txt')
575     expected = {
576       u'algo': u'sha-1',
577       u'files': {key: files[key]},
578       u'version': unicode(isolate.isolateserver.ISOLATED_FILE_VERSION),
579     }
580     self.assertEqual(
581         expected, json.load(open(isolated_base + '.1.isolated', 'r')))
582
583   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
584   def do_not_test_touch_only(self):
585     self._execute(
586         'hashtable', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
587         False)
588     empty = os.path.join('files1', 'test_file1.txt')
589     self._expected_hash_tree(empty)
590     self._expected_isolated(['touch_only.py', 'gyp'], None, empty)
591
592   def test_touch_root(self):
593     self._execute('hashtable', 'touch_root.isolate', [], False)
594     self._expected_hash_tree(None)
595     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
596
597   def test_with_flag(self):
598     self._execute(
599         'hashtable', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
600         False)
601     self._expected_hash_tree(None)
602     self._expect_results(
603         ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
604
605   if sys.platform != 'win32':
606     def test_symlink_full(self):
607       self._execute('hashtable', 'symlink_full.isolate', [], False)
608       # Construct our own tree.
609       expected = [
610         str(v['h'])
611         for v in self._gen_files(False, None, False).itervalues() if 'h' in v
612       ]
613       expected.append(isolateserver.hash_file(self.isolated, ALGO))
614       self.assertEqual(sorted(expected), self._result_tree())
615       self._expect_results(['symlink_full.py'], None, None, None)
616
617     def test_symlink_partial(self):
618       self._execute('hashtable', 'symlink_partial.isolate', [], False)
619       # Construct our own tree.
620       expected = [
621         str(v['h'])
622         for v in self._gen_files(False, None, False).itervalues() if 'h' in v
623       ]
624       expected.append(isolateserver.hash_file(self.isolated, ALGO))
625       self.assertEqual(sorted(expected), self._result_tree())
626       self._expect_results(['symlink_partial.py'], None, None, None)
627
628     def test_symlink_outside_build_root(self):
629       self._execute(
630           'hashtable', 'symlink_outside_build_root.isolate', [], False)
631       # Construct our own tree.
632       expected = [
633         str(v['h'])
634         for v in self._gen_files(False, None, False).itervalues() if 'h' in v
635       ]
636       expected.append(isolateserver.hash_file(self.isolated, ALGO))
637       self.assertEqual(sorted(expected), self._result_tree())
638       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
639
640
641 class Isolate_remap(IsolateOutdir):
642   def test_fail(self):
643     self._execute('remap', 'fail.isolate', [], False)
644     self._expected_tree()
645     self._expect_results(['fail.py'], None, None, None)
646
647   def test_missing_trailing_slash(self):
648     self._test_missing_trailing_slash('remap')
649
650   def test_non_existent(self):
651     self._test_non_existent('remap')
652
653   def test_all_items_invalid(self):
654     out = self._test_all_items_invalid('remap')
655     self.assertTrue(out.startswith('Remapping'))
656     self._expected_tree()
657
658   def test_no_run(self):
659     self._execute('remap', 'no_run.isolate', [], False)
660     self._expected_tree()
661     self._expect_results([], None, None, None)
662
663   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
664   def do_not_test_touch_only(self):
665     self._execute(
666         'remap', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
667         False)
668     self._expected_tree()
669     empty = os.path.join('files1', 'test_file1.txt')
670     self._expect_results(
671         ['touch_only.py', 'gyp'], None, {u'FLAG': u'gyp'}, empty)
672
673   def test_touch_root(self):
674     self._execute('remap', 'touch_root.isolate', [], False)
675     self._expected_tree()
676     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
677
678   def test_with_flag(self):
679     self._execute(
680         'remap', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
681         False)
682     self._expected_tree()
683     self._expect_results(
684         ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
685
686   if sys.platform != 'win32':
687     def test_symlink_full(self):
688       self._execute('remap', 'symlink_full.isolate', [], False)
689       self._expected_tree()
690       self._expect_results(['symlink_full.py'], None, None, None)
691
692     def test_symlink_partial(self):
693       self._execute('remap', 'symlink_partial.isolate', [], False)
694       self._expected_tree()
695       self._expect_results(['symlink_partial.py'], None, None, None)
696
697     def test_symlink_outside_build_root(self):
698       self._execute('remap', 'symlink_outside_build_root.isolate', [], False)
699       self._expected_tree()
700       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
701
702
703 class Isolate_run(IsolateTempdir):
704   def test_fail(self):
705     try:
706       self._execute('run', 'fail.isolate', [], False)
707       self.fail()
708     except subprocess.CalledProcessError:
709       pass
710     self._expect_results(['fail.py'], None, None, None)
711
712   def test_missing_trailing_slash(self):
713     self._test_missing_trailing_slash('run')
714
715   def test_non_existent(self):
716     self._test_non_existent('run')
717
718   def test_all_items_invalid(self):
719     out = self._test_all_items_invalid('run')
720     self.assertEqual('', out)
721
722   def test_no_run(self):
723     try:
724       self._execute('run', 'no_run.isolate', [], False)
725       self.fail()
726     except subprocess.CalledProcessError:
727       pass
728     self._expect_no_result()
729
730   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
731   def do_not_test_touch_only(self):
732     self._execute(
733         'run', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'run'],
734         False)
735     empty = os.path.join('files1', 'test_file1.txt')
736     self._expect_results(
737         ['touch_only.py', 'run'], None, {u'FLAG': u'run'}, empty)
738
739   def test_touch_root(self):
740     self._execute('run', 'touch_root.isolate', [], False)
741     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
742
743   def test_with_flag(self):
744     self._execute(
745         'run', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'run'],
746         False)
747     self._expect_results(
748         ['with_flag.py', 'run'], None, {u'FLAG': u'run'}, None)
749
750   if sys.platform != 'win32':
751     def test_symlink_full(self):
752       self._execute('run', 'symlink_full.isolate', [], False)
753       self._expect_results(['symlink_full.py'], None, None, None)
754
755     def test_symlink_partial(self):
756       self._execute('run', 'symlink_partial.isolate', [], False)
757       self._expect_results(['symlink_partial.py'], None, None, None)
758
759     def test_symlink_outside_build_root(self):
760       self._execute('run', 'symlink_outside_build_root.isolate', [], False)
761       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
762
763
764 class Isolate_trace_read_merge(IsolateTempdir):
765   # Tests both trace, read and merge.
766   # Warning: merge updates .isolate files. But they are currently in their
767   # canonical format so they shouldn't be changed.
768   def _check_merge(self, filename):
769     filepath = file_path.get_native_path_case(
770             os.path.join(unicode(ROOT_DIR), 'tests', 'isolate', filename))
771     expected = 'Updating %s\n' % file_path.safe_relpath(filepath, self.tempdir)
772     with open(filepath, 'rb') as f:
773       old_content = f.read()
774     out = self._execute('merge', filename, [], True) or ''
775     self.assertEqual(expected, out)
776     with open(filepath, 'rb') as f:
777       new_content = f.read()
778     self.assertEqual(old_content, new_content)
779
780   @trace_test_util.check_can_trace
781   def test_fail(self):
782     # Even if the process returns non-zero, the trace will still be good.
783     try:
784       self._execute('trace', 'fail.isolate', ['-v'], True)
785       self.fail()
786     except subprocess.CalledProcessError, e:
787       self.assertEqual('', e.output)
788     self._expect_results(['fail.py'], None, None, None)
789     expected = _wrap_in_condition(
790         {
791           KEY_TRACKED: ['fail.py'],
792         })
793     out = self._execute('read', 'fail.isolate', [], True) or ''
794     self.assertEqual(expected.splitlines(), out.splitlines())
795     self._check_merge('fail.isolate')
796
797   def test_missing_trailing_slash(self):
798     self._test_missing_trailing_slash('trace')
799
800   def test_non_existent(self):
801     self._test_non_existent('trace')
802
803   @trace_test_util.check_can_trace
804   def test_all_items_invalid(self):
805     out = self._test_all_items_invalid('trace')
806     self.assertEqual('', out)
807
808   def test_no_run(self):
809     try:
810       self._execute('trace', 'no_run.isolate', [], True)
811       self.fail()
812     except subprocess.CalledProcessError, e:
813       out = e.output
814       err = e.stderr
815     self._expect_no_result()
816     expected = 'No command to run.'
817     self.assertEqual('', out)
818     self.assertIn(expected, err)
819
820   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
821   def do_not_test_touch_only(self):
822     out = self._execute(
823         'trace', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'trace'],
824         True)
825     self.assertEqual('', out)
826     empty = os.path.join('files1', 'test_file1.txt')
827     self._expect_results(
828         ['touch_only.py', 'trace'], None, {u'FLAG': u'trace'}, empty)
829     expected = {
830       KEY_TRACKED: ['touch_only.py'],
831       # Note that .isolate format mandates / and not os.path.sep.
832       KEY_TOUCHED: ['files1/test_file1.txt'],
833     }
834     if sys.platform != 'linux2':
835       # TODO(maruel): Implement touch-only tracing on non-linux.
836       del expected[KEY_TOUCHED]
837
838     out = self._execute('read', 'touch_only.isolate', [], True)
839     self.assertEqual(_wrap_in_condition(expected), out)
840     self._check_merge('touch_only.isolate')
841
842   @trace_test_util.check_can_trace
843   def test_touch_root(self):
844     out = self._execute('trace', 'touch_root.isolate', [], True)
845     self.assertEqual('', out)
846     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
847     expected = _wrap_in_condition(
848         {
849           KEY_TRACKED: [
850             '../../isolate.py',
851             'touch_root.py',
852           ],
853         })
854     out = self._execute('read', 'touch_root.isolate', [], True)
855     self.assertEqual(expected, out)
856     self._check_merge('touch_root.isolate')
857
858   @trace_test_util.check_can_trace
859   def test_with_flag(self):
860     out = self._execute(
861         'trace', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'trace'],
862         True)
863     self.assertEqual('', out)
864     self._expect_results(
865         ['with_flag.py', 'trace'], None, {u'FLAG': u'trace'}, None)
866     expected = {
867       KEY_TRACKED: ['with_flag.py'],
868       # Note that .isolate format mandates / and not os.path.sep.
869       KEY_UNTRACKED: ['files1/'],
870     }
871     out = self._execute('read', 'with_flag.isolate', [], True)
872     self.assertEqual(_wrap_in_condition(expected), out)
873     self._check_merge('with_flag.isolate')
874
875   if sys.platform != 'win32':
876     def test_symlink_full(self):
877       out = self._execute(
878           'trace', 'symlink_full.isolate', [], True)
879       self.assertEqual('', out)
880       self._expect_results(['symlink_full.py'], None, None, None)
881       expected = {
882         KEY_TRACKED: ['symlink_full.py'],
883         # Note that .isolate format mandates / and not os.path.sep.
884         KEY_UNTRACKED: ['files2/'],
885       }
886       out = self._execute('read', 'symlink_full.isolate', [], True)
887       self.assertEqual(_wrap_in_condition(expected), out)
888       self._check_merge('symlink_full.isolate')
889
890     def test_symlink_partial(self):
891       out = self._execute(
892           'trace', 'symlink_partial.isolate', [], True)
893       self.assertEqual('', out)
894       self._expect_results(['symlink_partial.py'], None, None, None)
895       expected = {
896         KEY_TRACKED: ['symlink_partial.py'],
897         KEY_UNTRACKED: ['files2/test_file2.txt'],
898       }
899       out = self._execute('read', 'symlink_partial.isolate', [], True)
900       self.assertEqual(_wrap_in_condition(expected), out)
901       self._check_merge('symlink_partial.isolate')
902
903     def test_symlink_outside_build_root(self):
904       out = self._execute(
905           'trace', 'symlink_outside_build_root.isolate', [], True)
906       self.assertEqual('', out)
907       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
908       expected = {
909         KEY_TRACKED: ['symlink_outside_build_root.py'],
910         KEY_UNTRACKED: ['link_outside_build_root/'],
911       }
912       out = self._execute(
913           'read', 'symlink_outside_build_root.isolate', [], True)
914       self.assertEqual(_wrap_in_condition(expected), out)
915       self._check_merge('symlink_outside_build_root.isolate')
916
917
918 class IsolateNoOutdir(IsolateTempdir):
919   # Test without the --outdir flag.
920   # So all the files are first copied in the tempdir and the test is run from
921   # there.
922   def setUp(self):
923     super(IsolateNoOutdir, self).setUp()
924     self.root = os.path.join(self.tempdir, 'root')
925     os.makedirs(os.path.join(self.root, 'tests', 'isolate'))
926     for i in ('touch_root.isolate', 'touch_root.py'):
927       shutil.copy(
928           os.path.join(ROOT_DIR, 'tests', 'isolate', i),
929           os.path.join(self.root, 'tests', 'isolate', i))
930       shutil.copy(
931           os.path.join(ROOT_DIR, 'isolate.py'),
932           os.path.join(self.root, 'isolate.py'))
933
934   def _execute(self, mode, args, need_output):  # pylint: disable=W0221
935     """Executes isolate.py."""
936     cmd = [
937       sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
938       mode,
939       '--isolated', self.isolated,
940       '--config-variable', 'OS', 'mac',
941       '--config-variable', 'chromeos', '0',
942     ]
943     cmd.extend(args)
944
945     env = os.environ.copy()
946     if 'ISOLATE_DEBUG' in env:
947       del env['ISOLATE_DEBUG']
948
949     if need_output or not VERBOSE:
950       stdout = subprocess.PIPE
951       stderr = subprocess.STDOUT
952     else:
953       cmd.extend(['-v'] * 3)
954       stdout = None
955       stderr = None
956
957     logging.debug(cmd)
958     cwd = self.tempdir
959     p = subprocess.Popen(
960         cmd,
961         stdout=stdout,
962         stderr=stderr,
963         cwd=cwd,
964         env=env,
965         universal_newlines=True)
966     out, err = p.communicate()
967     if p.returncode:
968       raise CalledProcessError(p.returncode, cmd, out, err, cwd)
969     return out
970
971   def mode(self):
972     """Returns the execution mode corresponding to this test case."""
973     test_id = self.id().split('.')
974     self.assertEqual(3, len(test_id))
975     self.assertEqual('__main__', test_id[0])
976     return re.match('^test_([a-z]+)$', test_id[2]).group(1)
977
978   def filename(self):
979     """Returns the filename corresponding to this test case."""
980     filename = os.path.join(self.root, 'tests', 'isolate', 'touch_root.isolate')
981     self.assertTrue(os.path.isfile(filename), filename)
982     return filename
983
984   def test_check(self):
985     self._execute('check', ['--isolate', self.filename()], False)
986     files = sorted([
987       'isolate_smoke_test.isolated',
988       'isolate_smoke_test.isolated.state',
989       os.path.join('root', 'tests', 'isolate', 'touch_root.isolate'),
990       os.path.join('root', 'tests', 'isolate', 'touch_root.py'),
991       os.path.join('root', 'isolate.py'),
992     ])
993     self.assertEqual(files, list_files_tree(self.tempdir))
994
995   def test_hashtable(self):
996     with self.assertRaises(CalledProcessError):
997       self._execute('hashtable', ['--isolate', self.filename()], False)
998
999   def test_remap(self):
1000     with self.assertRaises(CalledProcessError):
1001       self._execute('remap', ['--isolate', self.filename()], False)
1002
1003   def test_run(self):
1004     self._execute('run', ['--isolate', self.filename()], False)
1005     files = sorted([
1006       'isolate_smoke_test.isolated',
1007       'isolate_smoke_test.isolated.state',
1008       os.path.join('root', 'tests', 'isolate', 'touch_root.isolate'),
1009       os.path.join('root', 'tests', 'isolate', 'touch_root.py'),
1010       os.path.join('root', 'isolate.py'),
1011     ])
1012     self.assertEqual(files, list_files_tree(self.tempdir))
1013
1014   @trace_test_util.check_can_trace
1015   def test_trace_read_merge(self):
1016     self._execute('trace', ['--isolate', self.filename()], False)
1017     # Read the trace before cleaning up. No need to specify self.filename()
1018     # because add the needed information is in the .state file.
1019     output = self._execute('read', [], True)
1020     expected = {
1021       KEY_TRACKED: [
1022         '../../isolate.py',
1023         'touch_root.py',
1024       ],
1025     }
1026     self.assertEqual(_wrap_in_condition(expected), output)
1027
1028     output = self._execute('merge', [], True)
1029     expected = 'Updating %s\n' % os.path.join(
1030         'root', 'tests', 'isolate', 'touch_root.isolate')
1031     self.assertEqual(expected, output)
1032     # In theory the file is going to be updated but in practice its content
1033     # won't change.
1034
1035     # Clean the directory from the logs, which are OS-specific.
1036     isolate.trace_inputs.get_api().clean_trace(
1037         os.path.join(self.tempdir, 'isolate_smoke_test.isolated.log'))
1038     files = sorted([
1039       'isolate_smoke_test.isolated',
1040       'isolate_smoke_test.isolated.state',
1041       os.path.join('root', 'tests', 'isolate', 'touch_root.isolate'),
1042       os.path.join('root', 'tests', 'isolate', 'touch_root.py'),
1043       os.path.join('root', 'isolate.py'),
1044     ])
1045     self.assertEqual(files, list_files_tree(self.tempdir))
1046
1047
1048 class IsolateOther(IsolateTempdir):
1049   def test_run_mixed(self):
1050     # Test when a user mapped from a directory and then replay from another
1051     # directory. This is a very rare corner case.
1052     indir = os.path.join(self.tempdir, 'input')
1053     os.mkdir(indir)
1054     for i in ('simple.py', 'simple.isolate'):
1055       shutil.copy(
1056           os.path.join(ROOT_DIR, 'tests', 'isolate', i),
1057           os.path.join(indir, i))
1058     proc = subprocess.Popen(
1059         [
1060           sys.executable, 'isolate.py',
1061           'check',
1062           '-i', os.path.join(indir, 'simple.isolate'),
1063           '-s', os.path.join(indir, 'simple.isolated'),
1064           '--config-variable', 'OS', 'mac',
1065         ],
1066         stdout=subprocess.PIPE,
1067         stderr=subprocess.STDOUT,
1068         cwd=ROOT_DIR)
1069     stdout = proc.communicate()[0]
1070     self.assertEqual('', stdout)
1071     self.assertEqual(0, proc.returncode)
1072     expected = [
1073       'simple.isolate', 'simple.isolated', 'simple.isolated.state', 'simple.py',
1074     ]
1075     self.assertEqual(expected, sorted(os.listdir(indir)))
1076
1077     # Remove the original directory.
1078     indir2 = indir + '2'
1079     os.rename(indir, indir2)
1080
1081     # simple.isolated.state is required; it contains the variables.
1082     proc = subprocess.Popen(
1083         [
1084           sys.executable, 'isolate.py', 'run',
1085           '-s', os.path.join(indir2, 'simple.isolated'),
1086           '--skip-refresh',
1087         ],
1088         stdout=subprocess.PIPE,
1089         stderr=subprocess.STDOUT,
1090         cwd=ROOT_DIR,
1091         universal_newlines=True)
1092     stdout = proc.communicate()[0]
1093     self.assertEqual(1, proc.returncode)
1094     self.assertTrue('simple.py is missing' in stdout)
1095
1096   def test_empty_and_renamed(self):
1097     a_isolate = os.path.join(self.tempdir, 'a.isolate')
1098     with open(a_isolate, 'wb') as f:
1099       f.write('{}')
1100
1101     cmd = [
1102         sys.executable, 'isolate.py', 'check',
1103         '-s', os.path.join(self.tempdir, 'out.isolated'),
1104     ]
1105     subprocess.check_call(cmd + ['-i', a_isolate])
1106
1107     # Move the .isolate file aside and rerun the command with the new source but
1108     # same destination.
1109     b_isolate = os.path.join(self.tempdir, 'b.isolate')
1110     os.rename(a_isolate, b_isolate)
1111     subprocess.check_call(cmd + ['-i', b_isolate])
1112
1113
1114 if __name__ == '__main__':
1115   VERBOSE = '-v' in sys.argv
1116   logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)
1117   if VERBOSE:
1118     unittest.TestCase.maxDiff = None
1119   unittest.main()