Upstream version 10.39.225.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 isolated_format
24 from utils import file_path
25
26
27 VERBOSE = False
28
29 ALGO = hashlib.sha1
30 HASH_NULL = ALGO().hexdigest()
31
32
33 # These are per test case, not per mode.
34 RELATIVE_CWD = {
35   'all_items_invalid': '.',
36   'fail': '.',
37   'missing_trailing_slash': '.',
38   'no_run': '.',
39   'non_existent': '.',
40   'split': '.',
41   'symlink_full': '.',
42   'symlink_partial': '.',
43   'symlink_outside_build_root': '.',
44   'touch_only': '.',
45   'touch_root': os.path.join('tests', 'isolate'),
46   'with_flag': '.',
47 }
48
49 DEPENDENCIES = {
50   'all_items_invalid' : ['empty.py'],
51   'fail': ['fail.py'],
52   'missing_trailing_slash': [],
53   'no_run': [
54     'no_run.isolate',
55     os.path.join('files1', 'subdir', '42.txt'),
56     os.path.join('files1', 'test_file1.txt'),
57     os.path.join('files1', 'test_file2.txt'),
58   ],
59   'non_existent': [],
60   'split': [
61     os.path.join('files1', 'subdir', '42.txt'),
62     os.path.join('test', 'data', 'foo.txt'),
63     'split.py',
64   ],
65   'symlink_full': [
66     os.path.join('files1', 'subdir', '42.txt'),
67     os.path.join('files1', 'test_file1.txt'),
68     os.path.join('files1', 'test_file2.txt'),
69     # files2 is a symlink to files1.
70     'files2',
71     'symlink_full.py',
72   ],
73   'symlink_partial': [
74     os.path.join('files1', 'test_file2.txt'),
75     # files2 is a symlink to files1.
76     'files2',
77     'symlink_partial.py',
78   ],
79   'symlink_outside_build_root': [
80     os.path.join('link_outside_build_root', 'test_file3.txt'),
81     'symlink_outside_build_root.py',
82   ],
83   'touch_only': [
84     'touch_only.py',
85     os.path.join('files1', 'test_file1.txt'),
86   ],
87   'touch_root': [
88     os.path.join('tests', 'isolate', 'touch_root.py'),
89     'isolate.py',
90   ],
91   'with_flag': [
92     'with_flag.py',
93     os.path.join('files1', 'subdir', '42.txt'),
94     os.path.join('files1', 'test_file1.txt'),
95     os.path.join('files1', 'test_file2.txt'),
96   ],
97 }
98
99
100 class CalledProcessError(subprocess.CalledProcessError):
101   """Makes 2.6 version act like 2.7"""
102   def __init__(self, returncode, cmd, output, stderr, cwd):
103     super(CalledProcessError, self).__init__(returncode, cmd)
104     self.output = output
105     self.stderr = stderr
106     self.cwd = cwd
107
108   def __str__(self):
109     return super(CalledProcessError, self).__str__() + (
110         '\n'
111         'cwd=%s\n%s\n%s\n%s') % (
112             self.cwd,
113             self.output,
114             self.stderr,
115             ' '.join(self.cmd))
116
117
118 def list_files_tree(directory):
119   """Returns the list of all the files in a tree."""
120   actual = []
121   for root, dirnames, filenames in os.walk(directory):
122     actual.extend(os.path.join(root, f)[len(directory)+1:] for f in filenames)
123     for dirname in dirnames:
124       full = os.path.join(root, dirname)
125       # Manually include symlinks.
126       if os.path.islink(full):
127         actual.append(full[len(directory)+1:])
128   return sorted(actual)
129
130
131 def _isolate_dict_to_string(values):
132   buf = cStringIO.StringIO()
133   isolate.isolate_format.pretty_print(values, buf)
134   return buf.getvalue()
135
136
137 def _wrap_in_condition(variables):
138   """Wraps a variables dict inside the current OS condition.
139
140   Returns the equivalent string.
141   """
142   return _isolate_dict_to_string(
143       {
144         'conditions': [
145           ['OS=="mac" and chromeos==0', {
146             'variables': variables
147           }],
148         ],
149       })
150
151
152 def _fix_file_mode(filename, read_only):
153   """4 modes are supported, 0750 (rwx), 0640 (rw), 0550 (rx), 0440 (r)."""
154   min_mode = 0440
155   if not read_only:
156     min_mode |= 0200
157   return (min_mode | 0110) if filename.endswith('.py') else min_mode
158
159
160 class Isolate(unittest.TestCase):
161   def test_help_modes(self):
162     # Check coherency in the help and implemented modes.
163     p = subprocess.Popen(
164         [sys.executable, os.path.join(ROOT_DIR, 'isolate.py'), '--help'],
165         stdout=subprocess.PIPE,
166         stderr=subprocess.STDOUT,
167         cwd=ROOT_DIR)
168     out = p.communicate()[0].splitlines()
169     self.assertEqual(0, p.returncode)
170     out = out[out.index('Commands are:') + 1:]
171     out = out[:out.index('')]
172     regexp = '^  (?:\x1b\\[\\d\\dm|)(\\w+)\s*(:?\x1b\\[\\d\\dm|) .+'
173     modes = [re.match(regexp, l) for l in out]
174     modes = [m.group(1) for m in modes if m]
175     EXPECTED_MODES = (
176         'archive',
177         'check',
178         'help',
179         'remap',
180         'rewrite',
181         'run',
182     )
183     # If a new command is added it should at least has a bare test.
184     self.assertEqual(sorted(EXPECTED_MODES), sorted(modes))
185
186
187 class IsolateTempdir(unittest.TestCase):
188   def setUp(self):
189     super(IsolateTempdir, self).setUp()
190     self.tempdir = tempfile.mkdtemp(prefix='isolate_smoke_')
191     self.isolated = os.path.join(self.tempdir, 'isolate_smoke_test.isolated')
192
193   def tearDown(self):
194     try:
195       logging.debug(self.tempdir)
196       shutil.rmtree(self.tempdir)
197     finally:
198       super(IsolateTempdir, self).tearDown()
199
200   def _gen_files(self, read_only, empty_file, with_time):
201     """Returns a dict of files like calling isolate.files_to_metadata() on each
202     file.
203
204     Arguments:
205     - read_only: Mark all the 'm' modes without the writeable bit.
206     - empty_file: Add a specific empty file (size 0).
207     - with_time: Include 't' timestamps. For saved state .state files.
208     """
209     root_dir = ROOT_DIR
210     if RELATIVE_CWD[self.case()] == '.':
211       root_dir = os.path.join(root_dir, 'tests', 'isolate')
212
213     files = dict((unicode(f), {}) for f in DEPENDENCIES[self.case()])
214
215     for relfile, v in files.iteritems():
216       filepath = os.path.join(root_dir, relfile)
217       filestats = os.lstat(filepath)
218       is_link = stat.S_ISLNK(filestats.st_mode)
219       if not is_link:
220         v[u's'] = filestats.st_size
221         if sys.platform != 'win32':
222           v[u'm'] = _fix_file_mode(relfile, read_only)
223       if with_time:
224         # Used to skip recalculating the hash. Use the most recent update
225         # time.
226         v[u't'] = int(round(filestats.st_mtime))
227       if is_link:
228         v[u'l'] = os.readlink(filepath)  # pylint: disable=E1101
229       else:
230         # Upgrade the value to unicode so diffing the structure in case of
231         # test failure is easier, since the basestring type must match,
232         # str!=unicode.
233         v[u'h'] = unicode(isolated_format.hash_file(filepath, ALGO))
234
235     if empty_file:
236       item = files[empty_file]
237       item['h'] = unicode(HASH_NULL)
238       if sys.platform != 'win32':
239         item['m'] = 288
240       item['s'] = 0
241       if with_time:
242         item['T'] = True
243         item.pop('t', None)
244     return files
245
246   def _expected_isolated(self, args, read_only, empty_file):
247     """Verifies self.isolated contains the expected data."""
248     expected = {
249       u'algo': u'sha-1',
250       u'files': self._gen_files(read_only, empty_file, False),
251       u'relative_cwd': unicode(RELATIVE_CWD[self.case()]),
252       u'version': unicode(isolated_format.ISOLATED_FILE_VERSION),
253     }
254     if read_only is not None:
255       expected[u'read_only'] = read_only
256     if args:
257       expected[u'command'] = [u'python'] + [unicode(x) for x in args]
258     self.assertEqual(expected, json.load(open(self.isolated, 'r')))
259
260   def _expected_saved_state(
261       self, args, read_only, empty_file, extra_vars, root_dir):
262     expected = {
263       u'OS': unicode(sys.platform),
264       u'algo': u'sha-1',
265       u'child_isolated_files': [],
266       u'command': [],
267       u'config_variables': {
268         u'OS': u'mac',
269         u'chromeos': 0,
270       },
271       u'extra_variables': {
272         u'EXECUTABLE_SUFFIX': u'.exe' if sys.platform == 'win32' else u'',
273       },
274       u'files': self._gen_files(read_only, empty_file, True),
275       u'isolate_file': file_path.safe_relpath(
276           file_path.get_native_path_case(unicode(self.filename())),
277           unicode(os.path.dirname(self.isolated))),
278       u'path_variables': {},
279       u'relative_cwd': unicode(RELATIVE_CWD[self.case()]),
280       u'root_dir': unicode(root_dir or os.path.dirname(self.filename())),
281       u'version': unicode(isolate.SavedState.EXPECTED_VERSION),
282     }
283     if args:
284       expected[u'command'] = [u'python'] + [unicode(x) for x in args]
285     expected['extra_variables'].update(extra_vars or {})
286     self.assertEqual(expected, json.load(open(self.saved_state(), 'r')))
287
288   def _expect_results(
289       self, args, read_only, extra_vars, empty_file, root_dir=None):
290     self._expected_isolated(args, read_only, empty_file)
291     self._expected_saved_state(
292         args, read_only, empty_file, extra_vars, root_dir)
293     # Also verifies run_isolated.py will be able to read it.
294     with open(self.isolated, 'rb') as f:
295       isolated_format.load_isolated(f.read(), ALGO)
296
297   def _expect_no_result(self):
298     self.assertFalse(os.path.exists(self.isolated))
299
300   def _get_cmd(self, mode):
301     return [
302       sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
303       mode,
304       '--isolated', self.isolated,
305       '--isolate', self.filename(),
306       '--config-variable', 'OS', 'mac',
307       '--config-variable', 'chromeos', '0',
308     ]
309
310   def _execute(self, mode, case, args, need_output, cwd=ROOT_DIR):
311     """Executes isolate.py."""
312     self.assertEqual(
313         case,
314         self.case() + '.isolate',
315         'Rename the test case to test_%s()' % case)
316     cmd = self._get_cmd(mode)
317     cmd.extend(args)
318
319     env = os.environ.copy()
320     if 'ISOLATE_DEBUG' in env:
321       del env['ISOLATE_DEBUG']
322
323     if need_output or not VERBOSE:
324       stdout = subprocess.PIPE
325       stderr = subprocess.PIPE
326     else:
327       cmd.extend(['-v'] * 3)
328       stdout = None
329       stderr = None
330
331     logging.debug(cmd)
332     p = subprocess.Popen(
333         cmd,
334         stdout=stdout,
335         stderr=stderr,
336         cwd=cwd,
337         env=env,
338         universal_newlines=True)
339     out, err = p.communicate()
340     if p.returncode:
341       raise CalledProcessError(p.returncode, cmd, out, err, cwd)
342
343     # Do not check on Windows since a lot of spew is generated there.
344     if sys.platform != 'win32':
345       self.assertTrue(err in (None, ''), err)
346     return out
347
348   def case(self):
349     """Returns the filename corresponding to this test case."""
350     test_id = self.id().split('.')
351     return re.match('^test_([a-z_]+)$', test_id[2]).group(1)
352
353   def filename(self):
354     """Returns the filename corresponding to this test case."""
355     filename = os.path.join(
356         ROOT_DIR, 'tests', 'isolate', self.case() + '.isolate')
357     self.assertTrue(os.path.isfile(filename), filename)
358     return filename
359
360   def saved_state(self):
361     return isolate.isolatedfile_to_state(self.isolated)
362
363   def _test_all_items_invalid(self, mode):
364     out = self._execute(mode, 'all_items_invalid.isolate',
365                         ['--ignore_broken_item'], True)
366     self._expect_results(['empty.py'], None, None, None)
367
368     return out or ''
369
370   def _test_missing_trailing_slash(self, mode):
371     try:
372       self._execute(mode, 'missing_trailing_slash.isolate', [], True)
373       self.fail()
374     except subprocess.CalledProcessError as e:
375       self.assertEqual('', e.output)
376       out = e.stderr
377     self._expect_no_result()
378     root = file_path.get_native_path_case(unicode(ROOT_DIR))
379     expected = (
380       'Input directory %s must have a trailing slash' %
381           os.path.join(root, 'tests', 'isolate', 'files1')
382     )
383     self.assertIn(expected, out)
384
385   def _test_non_existent(self, mode):
386     try:
387       self._execute(mode, 'non_existent.isolate', [], True)
388       self.fail()
389     except subprocess.CalledProcessError as e:
390       self.assertEqual('', e.output)
391       out = e.stderr
392     self._expect_no_result()
393     root = file_path.get_native_path_case(unicode(ROOT_DIR))
394     expected = (
395       'Input file %s doesn\'t exist' %
396           os.path.join(root, 'tests', 'isolate', 'A_file_that_do_not_exist')
397     )
398     self.assertIn(expected, out)
399
400
401 class IsolateOutdir(IsolateTempdir):
402   def setUp(self):
403     super(IsolateOutdir, self).setUp()
404     # The tests assume the current directory is the file's directory.
405     os.chdir(ROOT_DIR)
406     self.outdir = os.path.join(self.tempdir, 'isolated')
407
408   def _expect_no_tree(self):
409     # No outdir was created.
410     self.assertFalse(os.path.exists(self.outdir))
411
412   def _result_tree(self):
413     return list_files_tree(self.outdir)
414
415   def _expected_tree(self):
416     """Verifies the files written in the temporary directory."""
417     self.assertEqual(sorted(DEPENDENCIES[self.case()]), self._result_tree())
418
419   def _get_cmd(self, mode):
420     """Adds --outdir for the commands supporting it."""
421     cmd = super(IsolateOutdir, self)._get_cmd(mode)
422     cmd.extend(('--outdir', self.outdir))
423     return cmd
424
425   def _test_missing_trailing_slash(self, mode):
426     super(IsolateOutdir, self)._test_missing_trailing_slash(mode)
427     self._expect_no_tree()
428
429   def _test_non_existent(self, mode):
430     super(IsolateOutdir, self)._test_non_existent(mode)
431     self._expect_no_tree()
432
433
434 class Isolate_check(IsolateTempdir):
435   def test_fail(self):
436     self._execute('check', 'fail.isolate', [], False)
437     self._expect_results(['fail.py'], None, None, None)
438
439   def test_missing_trailing_slash(self):
440     self._test_missing_trailing_slash('check')
441
442   def test_non_existent(self):
443     self._test_non_existent('check')
444
445   def test_all_items_invalid(self):
446     out = self._test_all_items_invalid('check')
447     self.assertEqual('', out)
448
449   def test_no_run(self):
450     self._execute('check', 'no_run.isolate', [], False)
451     self._expect_results([], None, None, None)
452
453   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
454   def do_not_test_touch_only(self):
455     self._execute(
456         'check', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
457         False)
458     empty = os.path.join('files1', 'test_file1.txt')
459     self._expected_isolated(['touch_only.py', 'gyp'], None, empty)
460
461   def test_touch_root(self):
462     self._execute('check', 'touch_root.isolate', [], False)
463     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
464
465   def test_with_flag(self):
466     self._execute(
467         'check', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
468         False)
469     self._expect_results(
470         ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
471
472   if sys.platform != 'win32':
473     def test_symlink_full(self):
474       self._execute('check', 'symlink_full.isolate', [], False)
475       self._expect_results(['symlink_full.py'], None, None, None)
476
477     def test_symlink_partial(self):
478       self._execute('check', 'symlink_partial.isolate', [], False)
479       self._expect_results(['symlink_partial.py'], None, None, None)
480
481     def test_symlink_outside_build_root(self):
482       self._execute('check', 'symlink_outside_build_root.isolate', [], False)
483       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
484
485
486 class Isolate_remap(IsolateOutdir):
487   def test_fail(self):
488     self._execute('remap', 'fail.isolate', [], False)
489     self._expected_tree()
490     self._expect_results(['fail.py'], None, None, None)
491
492   def test_missing_trailing_slash(self):
493     self._test_missing_trailing_slash('remap')
494
495   def test_non_existent(self):
496     self._test_non_existent('remap')
497
498   def test_all_items_invalid(self):
499     out = self._test_all_items_invalid('remap')
500     self.assertTrue(out.startswith('Remapping'))
501     self._expected_tree()
502
503   def test_no_run(self):
504     self._execute('remap', 'no_run.isolate', [], False)
505     self._expected_tree()
506     self._expect_results([], None, None, None)
507
508   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
509   def do_not_test_touch_only(self):
510     self._execute(
511         'remap', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
512         False)
513     self._expected_tree()
514     empty = os.path.join('files1', 'test_file1.txt')
515     self._expect_results(
516         ['touch_only.py', 'gyp'], None, {u'FLAG': u'gyp'}, empty)
517
518   def test_touch_root(self):
519     self._execute('remap', 'touch_root.isolate', [], False)
520     self._expected_tree()
521     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
522
523   def test_with_flag(self):
524     self._execute(
525         'remap', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
526         False)
527     self._expected_tree()
528     self._expect_results(
529         ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
530
531   if sys.platform != 'win32':
532     def test_symlink_full(self):
533       self._execute('remap', 'symlink_full.isolate', [], False)
534       self._expected_tree()
535       self._expect_results(['symlink_full.py'], None, None, None)
536
537     def test_symlink_partial(self):
538       self._execute('remap', 'symlink_partial.isolate', [], False)
539       self._expected_tree()
540       self._expect_results(['symlink_partial.py'], None, None, None)
541
542     def test_symlink_outside_build_root(self):
543       self._execute('remap', 'symlink_outside_build_root.isolate', [], False)
544       self._expected_tree()
545       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
546
547
548 class Isolate_run(IsolateTempdir):
549   def test_fail(self):
550     try:
551       self._execute('run', 'fail.isolate', [], False)
552       self.fail()
553     except subprocess.CalledProcessError:
554       pass
555     self._expect_results(['fail.py'], None, None, None)
556
557   def test_missing_trailing_slash(self):
558     self._test_missing_trailing_slash('run')
559
560   def test_non_existent(self):
561     self._test_non_existent('run')
562
563   def test_all_items_invalid(self):
564     out = self._test_all_items_invalid('run')
565     self.assertEqual('', out)
566
567   def test_no_run(self):
568     try:
569       self._execute('run', 'no_run.isolate', [], False)
570       self.fail()
571     except subprocess.CalledProcessError:
572       pass
573     self._expect_no_result()
574
575   # TODO(csharp): Disabled until crbug.com/150823 is fixed.
576   def do_not_test_touch_only(self):
577     self._execute(
578         'run', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'run'],
579         False)
580     empty = os.path.join('files1', 'test_file1.txt')
581     self._expect_results(
582         ['touch_only.py', 'run'], None, {u'FLAG': u'run'}, empty)
583
584   def test_touch_root(self):
585     self._execute('run', 'touch_root.isolate', [], False)
586     self._expect_results(['touch_root.py'], None, None, None, ROOT_DIR)
587
588   def test_with_flag(self):
589     self._execute(
590         'run', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'run'],
591         False)
592     self._expect_results(
593         ['with_flag.py', 'run'], None, {u'FLAG': u'run'}, None)
594
595   if sys.platform != 'win32':
596     def test_symlink_full(self):
597       self._execute('run', 'symlink_full.isolate', [], False)
598       self._expect_results(['symlink_full.py'], None, None, None)
599
600     def test_symlink_partial(self):
601       self._execute('run', 'symlink_partial.isolate', [], False)
602       self._expect_results(['symlink_partial.py'], None, None, None)
603
604     def test_symlink_outside_build_root(self):
605       self._execute('run', 'symlink_outside_build_root.isolate', [], False)
606       self._expect_results(['symlink_outside_build_root.py'], None, None, None)
607
608
609 class IsolateNoOutdir(IsolateTempdir):
610   # Test without the --outdir flag.
611   # So all the files are first copied in the tempdir and the test is run from
612   # there.
613   def setUp(self):
614     super(IsolateNoOutdir, self).setUp()
615     self.root = os.path.join(self.tempdir, 'root')
616     os.makedirs(os.path.join(self.root, 'tests', 'isolate'))
617     for i in ('touch_root.isolate', 'touch_root.py'):
618       shutil.copy(
619           os.path.join(ROOT_DIR, 'tests', 'isolate', i),
620           os.path.join(self.root, 'tests', 'isolate', i))
621       shutil.copy(
622           os.path.join(ROOT_DIR, 'isolate.py'),
623           os.path.join(self.root, 'isolate.py'))
624
625   def _execute(self, mode, args, need_output):  # pylint: disable=W0221
626     """Executes isolate.py."""
627     cmd = [
628       sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
629       mode,
630       '--isolated', self.isolated,
631       '--config-variable', 'OS', 'mac',
632       '--config-variable', 'chromeos', '0',
633     ]
634     cmd.extend(args)
635
636     env = os.environ.copy()
637     if 'ISOLATE_DEBUG' in env:
638       del env['ISOLATE_DEBUG']
639
640     if need_output or not VERBOSE:
641       stdout = subprocess.PIPE
642       stderr = subprocess.STDOUT
643     else:
644       cmd.extend(['-v'] * 3)
645       stdout = None
646       stderr = None
647
648     logging.debug(cmd)
649     cwd = self.tempdir
650     p = subprocess.Popen(
651         cmd,
652         stdout=stdout,
653         stderr=stderr,
654         cwd=cwd,
655         env=env,
656         universal_newlines=True)
657     out, err = p.communicate()
658     if p.returncode:
659       raise CalledProcessError(p.returncode, cmd, out, err, cwd)
660     return out
661
662   def mode(self):
663     """Returns the execution mode corresponding to this test case."""
664     test_id = self.id().split('.')
665     self.assertEqual(3, len(test_id))
666     self.assertEqual('__main__', test_id[0])
667     return re.match('^test_([a-z]+)$', test_id[2]).group(1)
668
669   def filename(self):
670     """Returns the filename corresponding to this test case."""
671     filename = os.path.join(self.root, 'tests', 'isolate', 'touch_root.isolate')
672     self.assertTrue(os.path.isfile(filename), filename)
673     return filename
674
675   def test_check(self):
676     self._execute('check', ['--isolate', self.filename()], False)
677     files = sorted([
678       'isolate_smoke_test.isolated',
679       'isolate_smoke_test.isolated.state',
680       os.path.join('root', 'tests', 'isolate', 'touch_root.isolate'),
681       os.path.join('root', 'tests', 'isolate', 'touch_root.py'),
682       os.path.join('root', 'isolate.py'),
683     ])
684     self.assertEqual(files, list_files_tree(self.tempdir))
685
686   def test_remap(self):
687     with self.assertRaises(CalledProcessError):
688       self._execute('remap', ['--isolate', self.filename()], False)
689
690   def test_run(self):
691     self._execute('run', ['--isolate', self.filename()], False)
692     files = sorted([
693       'isolate_smoke_test.isolated',
694       'isolate_smoke_test.isolated.state',
695       os.path.join('root', 'tests', 'isolate', 'touch_root.isolate'),
696       os.path.join('root', 'tests', 'isolate', 'touch_root.py'),
697       os.path.join('root', 'isolate.py'),
698     ])
699     self.assertEqual(files, list_files_tree(self.tempdir))
700
701
702 class IsolateOther(IsolateTempdir):
703   def test_run_mixed(self):
704     # Test when a user mapped from a directory and then replay from another
705     # directory. This is a very rare corner case.
706     indir = os.path.join(self.tempdir, 'input')
707     os.mkdir(indir)
708     for i in ('simple.py', 'simple.isolate'):
709       shutil.copy(
710           os.path.join(ROOT_DIR, 'tests', 'isolate', i),
711           os.path.join(indir, i))
712     proc = subprocess.Popen(
713         [
714           sys.executable, 'isolate.py',
715           'check',
716           '-i', os.path.join(indir, 'simple.isolate'),
717           '-s', os.path.join(indir, 'simple.isolated'),
718           '--config-variable', 'OS', 'mac',
719         ],
720         stdout=subprocess.PIPE,
721         stderr=subprocess.STDOUT,
722         cwd=ROOT_DIR)
723     stdout = proc.communicate()[0]
724     self.assertEqual('', stdout)
725     self.assertEqual(0, proc.returncode)
726     expected = [
727       'simple.isolate', 'simple.isolated', 'simple.isolated.state', 'simple.py',
728     ]
729     self.assertEqual(expected, sorted(os.listdir(indir)))
730
731     # Remove the original directory.
732     indir2 = indir + '2'
733     os.rename(indir, indir2)
734
735     # simple.isolated.state is required; it contains the variables.
736     proc = subprocess.Popen(
737         [
738           sys.executable, 'isolate.py', 'run',
739           '-s', os.path.join(indir2, 'simple.isolated'),
740           '--skip-refresh',
741         ],
742         stdout=subprocess.PIPE,
743         stderr=subprocess.STDOUT,
744         cwd=ROOT_DIR,
745         universal_newlines=True)
746     stdout = proc.communicate()[0]
747     self.assertEqual(1, proc.returncode)
748     self.assertTrue('simple.py is missing' in stdout)
749
750   def test_empty_and_renamed(self):
751     a_isolate = os.path.join(self.tempdir, 'a.isolate')
752     with open(a_isolate, 'wb') as f:
753       f.write('{}')
754
755     cmd = [
756         sys.executable, 'isolate.py', 'check',
757         '-s', os.path.join(self.tempdir, 'out.isolated'),
758     ]
759     subprocess.check_call(cmd + ['-i', a_isolate])
760
761     # Move the .isolate file aside and rerun the command with the new source but
762     # same destination.
763     b_isolate = os.path.join(self.tempdir, 'b.isolate')
764     os.rename(a_isolate, b_isolate)
765     subprocess.check_call(cmd + ['-i', b_isolate])
766
767
768 if __name__ == '__main__':
769   VERBOSE = '-v' in sys.argv
770   logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)
771   if VERBOSE:
772     unittest.TestCase.maxDiff = None
773   unittest.main()