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.
19 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 sys.path.insert(0, ROOT_DIR)
23 import isolated_format
24 from utils import file_path
30 HASH_NULL = ALGO().hexdigest()
33 # These are per test case, not per mode.
35 'all_items_invalid': '.',
37 'missing_trailing_slash': '.',
42 'symlink_partial': '.',
43 'symlink_outside_build_root': '.',
45 'touch_root': os.path.join('tests', 'isolate'),
50 'all_items_invalid' : ['empty.py'],
52 'missing_trailing_slash': [],
55 os.path.join('files1', 'subdir', '42.txt'),
56 os.path.join('files1', 'test_file1.txt'),
57 os.path.join('files1', 'test_file2.txt'),
61 os.path.join('files1', 'subdir', '42.txt'),
62 os.path.join('test', 'data', 'foo.txt'),
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.
74 os.path.join('files1', 'test_file2.txt'),
75 # files2 is a symlink to files1.
79 'symlink_outside_build_root': [
80 os.path.join('link_outside_build_root', 'test_file3.txt'),
81 'symlink_outside_build_root.py',
85 os.path.join('files1', 'test_file1.txt'),
88 os.path.join('tests', 'isolate', 'touch_root.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'),
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)
109 return super(CalledProcessError, self).__str__() + (
111 'cwd=%s\n%s\n%s\n%s') % (
118 def list_files_tree(directory):
119 """Returns the list of all the files in a tree."""
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)
131 def _isolate_dict_to_string(values):
132 buf = cStringIO.StringIO()
133 isolate.isolate_format.pretty_print(values, buf)
134 return buf.getvalue()
137 def _wrap_in_condition(variables):
138 """Wraps a variables dict inside the current OS condition.
140 Returns the equivalent string.
142 return _isolate_dict_to_string(
145 ['OS=="mac" and chromeos==0', {
146 'variables': variables
152 def _fix_file_mode(filename, read_only):
153 """4 modes are supported, 0750 (rwx), 0640 (rw), 0550 (rx), 0440 (r)."""
157 return (min_mode | 0110) if filename.endswith('.py') else min_mode
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,
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]
183 # If a new command is added it should at least has a bare test.
184 self.assertEqual(sorted(EXPECTED_MODES), sorted(modes))
187 class IsolateTempdir(unittest.TestCase):
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')
195 logging.debug(self.tempdir)
196 shutil.rmtree(self.tempdir)
198 super(IsolateTempdir, self).tearDown()
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
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.
210 if RELATIVE_CWD[self.case()] == '.':
211 root_dir = os.path.join(root_dir, 'tests', 'isolate')
213 files = dict((unicode(f), {}) for f in DEPENDENCIES[self.case()])
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)
220 v[u's'] = filestats.st_size
221 if sys.platform != 'win32':
222 v[u'm'] = _fix_file_mode(relfile, read_only)
224 # Used to skip recalculating the hash. Use the most recent update
226 v[u't'] = int(round(filestats.st_mtime))
228 v[u'l'] = os.readlink(filepath) # pylint: disable=E1101
230 # Upgrade the value to unicode so diffing the structure in case of
231 # test failure is easier, since the basestring type must match,
233 v[u'h'] = unicode(isolated_format.hash_file(filepath, ALGO))
236 item = files[empty_file]
237 item['h'] = unicode(HASH_NULL)
238 if sys.platform != 'win32':
246 def _expected_isolated(self, args, read_only, empty_file):
247 """Verifies self.isolated contains the expected data."""
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),
254 if read_only is not None:
255 expected[u'read_only'] = read_only
257 expected[u'command'] = [u'python'] + [unicode(x) for x in args]
258 self.assertEqual(expected, json.load(open(self.isolated, 'r')))
260 def _expected_saved_state(
261 self, args, read_only, empty_file, extra_vars, root_dir):
263 u'OS': unicode(sys.platform),
265 u'child_isolated_files': [],
267 u'config_variables': {
271 u'extra_variables': {
272 u'EXECUTABLE_SUFFIX': u'.exe' if sys.platform == 'win32' else u'',
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),
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')))
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)
297 def _expect_no_result(self):
298 self.assertFalse(os.path.exists(self.isolated))
300 def _get_cmd(self, mode):
302 sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
304 '--isolated', self.isolated,
305 '--isolate', self.filename(),
306 '--config-variable', 'OS', 'mac',
307 '--config-variable', 'chromeos', '0',
310 def _execute(self, mode, case, args, need_output, cwd=ROOT_DIR):
311 """Executes isolate.py."""
314 self.case() + '.isolate',
315 'Rename the test case to test_%s()' % case)
316 cmd = self._get_cmd(mode)
319 env = os.environ.copy()
320 if 'ISOLATE_DEBUG' in env:
321 del env['ISOLATE_DEBUG']
323 if need_output or not VERBOSE:
324 stdout = subprocess.PIPE
325 stderr = subprocess.PIPE
327 cmd.extend(['-v'] * 3)
332 p = subprocess.Popen(
338 universal_newlines=True)
339 out, err = p.communicate()
341 raise CalledProcessError(p.returncode, cmd, out, err, cwd)
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)
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)
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)
360 def saved_state(self):
361 return isolate.isolatedfile_to_state(self.isolated)
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)
370 def _test_missing_trailing_slash(self, mode):
372 self._execute(mode, 'missing_trailing_slash.isolate', [], True)
374 except subprocess.CalledProcessError as e:
375 self.assertEqual('', e.output)
377 self._expect_no_result()
378 root = file_path.get_native_path_case(unicode(ROOT_DIR))
380 'Input directory %s must have a trailing slash' %
381 os.path.join(root, 'tests', 'isolate', 'files1')
383 self.assertIn(expected, out)
385 def _test_non_existent(self, mode):
387 self._execute(mode, 'non_existent.isolate', [], True)
389 except subprocess.CalledProcessError as e:
390 self.assertEqual('', e.output)
392 self._expect_no_result()
393 root = file_path.get_native_path_case(unicode(ROOT_DIR))
395 'Input file %s doesn\'t exist' %
396 os.path.join(root, 'tests', 'isolate', 'A_file_that_do_not_exist')
398 self.assertIn(expected, out)
401 class IsolateOutdir(IsolateTempdir):
403 super(IsolateOutdir, self).setUp()
404 # The tests assume the current directory is the file's directory.
406 self.outdir = os.path.join(self.tempdir, 'isolated')
408 def _expect_no_tree(self):
409 # No outdir was created.
410 self.assertFalse(os.path.exists(self.outdir))
412 def _result_tree(self):
413 return list_files_tree(self.outdir)
415 def _expected_tree(self):
416 """Verifies the files written in the temporary directory."""
417 self.assertEqual(sorted(DEPENDENCIES[self.case()]), self._result_tree())
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))
425 def _test_missing_trailing_slash(self, mode):
426 super(IsolateOutdir, self)._test_missing_trailing_slash(mode)
427 self._expect_no_tree()
429 def _test_non_existent(self, mode):
430 super(IsolateOutdir, self)._test_non_existent(mode)
431 self._expect_no_tree()
434 class Isolate_check(IsolateTempdir):
436 self._execute('check', 'fail.isolate', [], False)
437 self._expect_results(['fail.py'], None, None, None)
439 def test_missing_trailing_slash(self):
440 self._test_missing_trailing_slash('check')
442 def test_non_existent(self):
443 self._test_non_existent('check')
445 def test_all_items_invalid(self):
446 out = self._test_all_items_invalid('check')
447 self.assertEqual('', out)
449 def test_no_run(self):
450 self._execute('check', 'no_run.isolate', [], False)
451 self._expect_results([], None, None, None)
453 # TODO(csharp): Disabled until crbug.com/150823 is fixed.
454 def do_not_test_touch_only(self):
456 'check', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
458 empty = os.path.join('files1', 'test_file1.txt')
459 self._expected_isolated(['touch_only.py', 'gyp'], None, empty)
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)
465 def test_with_flag(self):
467 'check', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
469 self._expect_results(
470 ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
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)
477 def test_symlink_partial(self):
478 self._execute('check', 'symlink_partial.isolate', [], False)
479 self._expect_results(['symlink_partial.py'], None, None, None)
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)
486 class Isolate_remap(IsolateOutdir):
488 self._execute('remap', 'fail.isolate', [], False)
489 self._expected_tree()
490 self._expect_results(['fail.py'], None, None, None)
492 def test_missing_trailing_slash(self):
493 self._test_missing_trailing_slash('remap')
495 def test_non_existent(self):
496 self._test_non_existent('remap')
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()
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)
508 # TODO(csharp): Disabled until crbug.com/150823 is fixed.
509 def do_not_test_touch_only(self):
511 'remap', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'gyp'],
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)
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)
523 def test_with_flag(self):
525 'remap', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'gyp'],
527 self._expected_tree()
528 self._expect_results(
529 ['with_flag.py', 'gyp'], None, {u'FLAG': u'gyp'}, None)
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)
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)
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)
548 class Isolate_run(IsolateTempdir):
551 self._execute('run', 'fail.isolate', [], False)
553 except subprocess.CalledProcessError:
555 self._expect_results(['fail.py'], None, None, None)
557 def test_missing_trailing_slash(self):
558 self._test_missing_trailing_slash('run')
560 def test_non_existent(self):
561 self._test_non_existent('run')
563 def test_all_items_invalid(self):
564 out = self._test_all_items_invalid('run')
565 self.assertEqual('', out)
567 def test_no_run(self):
569 self._execute('run', 'no_run.isolate', [], False)
571 except subprocess.CalledProcessError:
573 self._expect_no_result()
575 # TODO(csharp): Disabled until crbug.com/150823 is fixed.
576 def do_not_test_touch_only(self):
578 'run', 'touch_only.isolate', ['--extra-variable', 'FLAG', 'run'],
580 empty = os.path.join('files1', 'test_file1.txt')
581 self._expect_results(
582 ['touch_only.py', 'run'], None, {u'FLAG': u'run'}, empty)
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)
588 def test_with_flag(self):
590 'run', 'with_flag.isolate', ['--extra-variable', 'FLAG', 'run'],
592 self._expect_results(
593 ['with_flag.py', 'run'], None, {u'FLAG': u'run'}, None)
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)
600 def test_symlink_partial(self):
601 self._execute('run', 'symlink_partial.isolate', [], False)
602 self._expect_results(['symlink_partial.py'], None, None, None)
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)
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
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'):
619 os.path.join(ROOT_DIR, 'tests', 'isolate', i),
620 os.path.join(self.root, 'tests', 'isolate', i))
622 os.path.join(ROOT_DIR, 'isolate.py'),
623 os.path.join(self.root, 'isolate.py'))
625 def _execute(self, mode, args, need_output): # pylint: disable=W0221
626 """Executes isolate.py."""
628 sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
630 '--isolated', self.isolated,
631 '--config-variable', 'OS', 'mac',
632 '--config-variable', 'chromeos', '0',
636 env = os.environ.copy()
637 if 'ISOLATE_DEBUG' in env:
638 del env['ISOLATE_DEBUG']
640 if need_output or not VERBOSE:
641 stdout = subprocess.PIPE
642 stderr = subprocess.STDOUT
644 cmd.extend(['-v'] * 3)
650 p = subprocess.Popen(
656 universal_newlines=True)
657 out, err = p.communicate()
659 raise CalledProcessError(p.returncode, cmd, out, err, cwd)
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)
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)
675 def test_check(self):
676 self._execute('check', ['--isolate', self.filename()], False)
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'),
684 self.assertEqual(files, list_files_tree(self.tempdir))
686 def test_remap(self):
687 with self.assertRaises(CalledProcessError):
688 self._execute('remap', ['--isolate', self.filename()], False)
691 self._execute('run', ['--isolate', self.filename()], False)
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'),
699 self.assertEqual(files, list_files_tree(self.tempdir))
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')
708 for i in ('simple.py', 'simple.isolate'):
710 os.path.join(ROOT_DIR, 'tests', 'isolate', i),
711 os.path.join(indir, i))
712 proc = subprocess.Popen(
714 sys.executable, 'isolate.py',
716 '-i', os.path.join(indir, 'simple.isolate'),
717 '-s', os.path.join(indir, 'simple.isolated'),
718 '--config-variable', 'OS', 'mac',
720 stdout=subprocess.PIPE,
721 stderr=subprocess.STDOUT,
723 stdout = proc.communicate()[0]
724 self.assertEqual('', stdout)
725 self.assertEqual(0, proc.returncode)
727 'simple.isolate', 'simple.isolated', 'simple.isolated.state', 'simple.py',
729 self.assertEqual(expected, sorted(os.listdir(indir)))
731 # Remove the original directory.
733 os.rename(indir, indir2)
735 # simple.isolated.state is required; it contains the variables.
736 proc = subprocess.Popen(
738 sys.executable, 'isolate.py', 'run',
739 '-s', os.path.join(indir2, 'simple.isolated'),
742 stdout=subprocess.PIPE,
743 stderr=subprocess.STDOUT,
745 universal_newlines=True)
746 stdout = proc.communicate()[0]
747 self.assertEqual(1, proc.returncode)
748 self.assertTrue('simple.py is missing' in stdout)
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:
756 sys.executable, 'isolate.py', 'check',
757 '-s', os.path.join(self.tempdir, 'out.isolated'),
759 subprocess.check_call(cmd + ['-i', a_isolate])
761 # Move the .isolate file aside and rerun the command with the new source but
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])
768 if __name__ == '__main__':
769 VERBOSE = '-v' in sys.argv
770 logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)
772 unittest.TestCase.maxDiff = None