3 # Copyright 2012 The Swarming Authors. All rights reserved.
4 # Use of this source code is governed under the Apache License, Version 2.0 that
5 # can be found in the LICENSE file.
18 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19 sys.path.insert(0, ROOT_DIR)
22 from utils import file_path
23 from utils import threading_utils
26 FILENAME = os.path.basename(__file__)
27 REL_DATA = os.path.join(u'tests', 'trace_inputs')
30 # TODO(maruel): Have the kernel tracer on Windows differentiate between file
32 MODE_R = trace_inputs.Results.File.READ if sys.platform != 'win32' else None
33 MODE_W = trace_inputs.Results.File.WRITE if sys.platform != 'win32' else None
34 MODE_T = trace_inputs.Results.File.TOUCHED
37 def check_can_trace(fn):
38 """Function decorator that skips test that need to be able trace."""
40 def hook(self, *args, **kwargs):
41 if not trace_inputs.can_trace():
42 self.fail('Please rerun this test with admin privileges.')
43 return fn(self, *args, **kwargs)
47 class CalledProcessError(subprocess.CalledProcessError):
48 """Makes 2.6 version act like 2.7"""
49 def __init__(self, returncode, cmd, output, cwd):
50 super(CalledProcessError, self).__init__(returncode, cmd)
55 return super(CalledProcessError, self).__str__() + (
57 'cwd=%s\n%s') % (self.cwd, self.output)
60 class TraceInputsBase(unittest.TestCase):
63 self.trace_inputs_path = os.path.join(ROOT_DIR, 'trace_inputs.py')
65 # Wraps up all the differences between OSes here.
66 # - Windows doesn't track initial_cwd.
67 # - OSX replaces /usr/bin/python with /usr/bin/python2.7.
68 self.cwd = os.path.join(ROOT_DIR, u'tests')
69 self.initial_cwd = unicode(self.cwd)
70 self.expected_cwd = unicode(ROOT_DIR)
71 if sys.platform == 'win32':
72 # Not supported on Windows.
73 self.initial_cwd = None
74 self.expected_cwd = None
76 # There's 3 kinds of references to python, self.executable,
77 # self.real_executable and self.naked_executable. It depends how python was
79 self.executable = sys.executable
80 if sys.platform == 'darwin':
81 # /usr/bin/python is a thunk executable that decides which version of
82 # python gets executed.
83 suffix = '.'.join(map(str, sys.version_info[0:2]))
84 if os.access(self.executable + suffix, os.X_OK):
85 # So it'll look like /usr/bin/python2.7
86 self.executable += suffix
88 self.real_executable = file_path.get_native_path_case(
89 unicode(self.executable))
90 self.tempdir = file_path.get_native_path_case(
91 unicode(tempfile.mkdtemp(prefix='trace_smoke_test')))
92 self.log = os.path.join(self.tempdir, 'log')
94 # self.naked_executable will only be naked on Windows.
95 self.naked_executable = unicode(sys.executable)
96 if sys.platform == 'win32':
97 self.naked_executable = os.path.basename(sys.executable)
102 print 'Leaking: %s' % self.tempdir
104 shutil.rmtree(self.tempdir)
107 def get_child_command(from_data):
108 """Returns command to run the child1.py."""
109 cmd = [sys.executable]
111 # When the gyp argument is specified, the command is started from --cwd
112 # directory. In this case, 'tests'.
113 cmd.extend([os.path.join('trace_inputs', 'child1.py'), '--child-gyp'])
115 # When the gyp argument is not specified, the command is started from
116 # --root-dir directory.
117 cmd.extend([os.path.join(REL_DATA, 'child1.py'), '--child'])
122 return os.stat(os.path.join(ROOT_DIR, *args)).st_size
125 class TraceInputs(TraceInputsBase):
126 def _execute(self, mode, command, cwd):
129 self.trace_inputs_path,
134 cmd.extend(['-v'] * 3)
136 logging.info('Command: %s' % ' '.join(cmd))
137 p = subprocess.Popen(
139 stdout=subprocess.PIPE,
140 stderr=subprocess.PIPE,
142 universal_newlines=True)
143 out, err = p.communicate()
147 raise CalledProcessError(p.returncode, cmd, out + err, cwd)
150 def _trace(self, from_data):
152 cwd = os.path.join(ROOT_DIR, 'tests')
155 return self._execute('trace', self.get_child_command(from_data), cwd=cwd)
158 def test_trace(self):
159 expected = '\n'.join((
162 'Interesting: 7 reduced to 6',
163 ' tests/trace_inputs/child1.py'.replace('/', os.path.sep),
164 ' tests/trace_inputs/child2.py'.replace('/', os.path.sep),
165 ' tests/trace_inputs/files1/'.replace('/', os.path.sep),
166 ' tests/trace_inputs/test_file.txt'.replace('/', os.path.sep),
167 (' tests/%s' % FILENAME).replace('/', os.path.sep),
170 trace_expected = '\n'.join((
171 'child from %s' % ROOT_DIR,
174 trace_actual = self._trace(False)
175 actual = self._execute(
178 '--root-dir', ROOT_DIR,
179 '--trace-blacklist', '.+\\.pyc',
180 '--trace-blacklist', '.*\\.svn',
181 '--trace-blacklist', '.*do_not_care\\.txt',
183 cwd=unicode(ROOT_DIR))
184 self.assertEqual(expected, actual)
185 self.assertEqual(trace_expected, trace_actual)
188 def test_trace_json(self):
194 u'command': [u'python', u'child2.py'],
195 u'executable': self.naked_executable,
199 u'path': os.path.join(REL_DATA, 'child2.py'),
200 u'size': self._size(REL_DATA, 'child2.py'),
204 u'path': os.path.join(REL_DATA, 'files1', 'bar'),
205 u'size': self._size(REL_DATA, 'files1', 'bar'),
209 u'path': os.path.join(REL_DATA, 'files1', 'foo'),
210 u'size': self._size(REL_DATA, 'files1', 'foo'),
214 u'path': os.path.join(REL_DATA, 'test_file.txt'),
215 u'size': self._size(REL_DATA, 'test_file.txt'),
218 u'initial_cwd': self.initial_cwd,
223 unicode(self.executable),
224 os.path.join(u'trace_inputs', 'child1.py'),
227 u'executable': self.real_executable,
231 u'path': os.path.join(REL_DATA, 'child1.py'),
232 u'size': self._size(REL_DATA, 'child1.py'),
236 u'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'),
237 u'size': self._size('tests', 'trace_inputs_smoke_test.py'),
241 u'path': u'trace_inputs.py',
242 u'size': self._size('trace_inputs.py'),
245 u'initial_cwd': self.initial_cwd,
249 trace_expected = '\n'.join((
250 'child_gyp from %s' % os.path.join(ROOT_DIR, 'tests'),
253 trace_actual = self._trace(True)
254 actual_text = self._execute(
257 '--root-dir', ROOT_DIR,
258 '--trace-blacklist', '.+\\.pyc',
259 '--trace-blacklist', '.*\\.svn',
260 '--trace-blacklist', '.*do_not_care\\.txt',
263 cwd=unicode(ROOT_DIR))
264 actual_json = json.loads(actual_text)
265 self.assertEqual(list, actual_json.__class__)
266 self.assertEqual(1, len(actual_json))
267 actual_json = actual_json[0]
269 self.assertTrue(actual_json['root'].pop('pid'))
270 self.assertTrue(actual_json['root']['children'][0].pop('pid'))
271 self.assertEqual(expected, actual_json)
272 self.assertEqual(trace_expected, trace_actual)
275 class TraceInputsImport(TraceInputsBase):
277 # Similar to TraceInputs test fixture except that it calls the function
278 # directly, so the Results instance can be inspected.
279 # Roughly, make sure the API is stable.
280 def _execute_trace(self, command):
281 # Similar to what trace_test_cases.py does.
282 api = trace_inputs.get_api()
283 _, _ = trace_inputs.trace(self.log, command, self.cwd, api, True)
284 # TODO(maruel): Check
285 #self.assertEqual(0, returncode)
286 #self.assertEqual('', output)
288 return f.endswith(('.pyc', '.svn', 'do_not_care.txt'))
289 data = api.parse_log(self.log, blacklist, None)
290 self.assertEqual(1, len(data))
291 if 'exception' in data[0]:
292 raise data[0]['exception'][0], \
293 data[0]['exception'][1], \
294 data[0]['exception'][2]
296 return data[0]['results'].strip_root(unicode(ROOT_DIR))
298 def _gen_dict_wrong_path(self):
299 """Returns the expected flattened Results when child1.py is called with the
307 os.path.join(REL_DATA, 'child1.py'),
310 'executable': self.real_executable,
312 'initial_cwd': self.initial_cwd,
316 def _gen_dict_full(self):
317 """Returns the expected flattened Results when child1.py is called with
325 'command': ['python', 'child2.py'],
326 'executable': self.naked_executable,
330 'path': os.path.join(REL_DATA, 'child2.py'),
331 'size': self._size(REL_DATA, 'child2.py'),
335 'path': os.path.join(REL_DATA, 'files1', 'bar'),
336 'size': self._size(REL_DATA, 'files1', 'bar'),
340 'path': os.path.join(REL_DATA, 'files1', 'foo'),
341 'size': self._size(REL_DATA, 'files1', 'foo'),
345 'path': os.path.join(REL_DATA, 'test_file.txt'),
346 'size': self._size(REL_DATA, 'test_file.txt'),
349 'initial_cwd': self.expected_cwd,
354 os.path.join(REL_DATA, 'child1.py'),
357 'executable': self.real_executable,
361 'path': os.path.join(REL_DATA, 'child1.py'),
362 'size': self._size(REL_DATA, 'child1.py'),
366 'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'),
367 'size': self._size('tests', 'trace_inputs_smoke_test.py'),
371 'path': u'trace_inputs.py',
372 'size': self._size('trace_inputs.py'),
375 'initial_cwd': self.expected_cwd,
379 def _gen_dict_full_gyp(self):
380 """Returns the expected flattened results when child1.py is called with
388 'command': [u'python', u'child2.py'],
389 'executable': self.naked_executable,
393 'path': os.path.join(REL_DATA, 'child2.py'),
394 'size': self._size(REL_DATA, 'child2.py'),
398 'path': os.path.join(REL_DATA, 'files1', 'bar'),
399 'size': self._size(REL_DATA, 'files1', 'bar'),
403 'path': os.path.join(REL_DATA, 'files1', 'foo'),
404 'size': self._size(REL_DATA, 'files1', 'foo'),
408 'path': os.path.join(REL_DATA, 'test_file.txt'),
409 'size': self._size(REL_DATA, 'test_file.txt'),
412 'initial_cwd': self.initial_cwd,
417 os.path.join('trace_inputs', 'child1.py'),
420 'executable': self.real_executable,
424 'path': os.path.join(REL_DATA, 'child1.py'),
425 'size': self._size(REL_DATA, 'child1.py'),
429 'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'),
430 'size': self._size('tests', 'trace_inputs_smoke_test.py'),
434 'path': u'trace_inputs.py',
435 'size': self._size('trace_inputs.py'),
438 'initial_cwd': self.initial_cwd,
443 def test_trace_wrong_path(self):
444 # Deliberately start the trace from the wrong path. Starts it from the
445 # directory 'tests' so 'tests/tests/trace_inputs/child1.py' is not
446 # accessible, so child2.py process is not started.
447 results = self._execute_trace(self.get_child_command(False))
448 expected = self._gen_dict_wrong_path()
449 actual = results.flatten()
450 self.assertTrue(actual['root'].pop('pid'))
451 self.assertEqual(expected, actual)
454 def test_trace(self):
455 expected = self._gen_dict_full_gyp()
456 results = self._execute_trace(self.get_child_command(True))
457 actual = results.flatten()
458 self.assertTrue(actual['root'].pop('pid'))
459 self.assertTrue(actual['root']['children'][0].pop('pid'))
460 self.assertEqual(expected, actual)
462 u'tests/trace_inputs/child1.py'.replace('/', os.path.sep),
463 u'tests/trace_inputs/child2.py'.replace('/', os.path.sep),
464 u'tests/trace_inputs/files1/'.replace('/', os.path.sep),
465 u'tests/trace_inputs/test_file.txt'.replace('/', os.path.sep),
466 u'tests/trace_inputs_smoke_test.py'.replace('/', os.path.sep),
470 return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn'))
471 simplified = trace_inputs.extract_directories(
472 file_path.get_native_path_case(unicode(ROOT_DIR)),
475 self.assertEqual(files, [f.path for f in simplified])
478 def test_trace_multiple(self):
479 # Starts parallel threads and trace parallel child processes simultaneously.
480 # Some are started from 'tests' directory, others from this script's
481 # directory. One trace fails. Verify everything still goes one.
484 def trace(tracer, cmd, cwd, tracename):
485 resultcode, output = tracer.trace(cmd, cwd, tracename, True)
486 return (tracename, resultcode, output)
488 with threading_utils.ThreadPool(parallel, parallel, 0) as pool:
489 api = trace_inputs.get_api()
490 with api.get_tracer(self.log) as tracer:
492 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace1')
494 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace2')
496 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace3')
498 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace4')
499 # Have this one fail since it's started from the wrong directory.
501 0, trace, tracer, self.get_child_command(False), self.cwd, 'trace5')
503 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace6')
505 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace7')
507 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace8')
508 trace_results = pool.join()
510 return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn'))
511 actual_results = api.parse_log(self.log, blacklist, None)
512 self.assertEqual(8, len(trace_results))
513 self.assertEqual(8, len(actual_results))
515 # Convert to dict keyed on the trace name, simpler to verify.
516 trace_results = dict((i[0], i[1:]) for i in trace_results)
517 actual_results = dict((x.pop('trace'), x) for x in actual_results)
518 self.assertEqual(sorted(trace_results), sorted(actual_results))
520 # It'd be nice to start different kinds of processes.
522 self._gen_dict_full(),
523 self._gen_dict_full_gyp(),
524 self._gen_dict_full(),
525 self._gen_dict_full_gyp(),
526 self._gen_dict_wrong_path(),
527 self._gen_dict_full_gyp(),
528 self._gen_dict_full(),
529 self._gen_dict_full_gyp(),
531 self.assertEqual(len(expected_results), len(trace_results))
533 # See the comment above about the trace that fails because it's started from
534 # the wrong directory.
536 for index, key in enumerate(sorted(actual_results)):
537 self.assertEqual('trace%d' % (index + 1), key)
538 self.assertEqual(2, len(trace_results[key]))
540 self.assertEqual(0 if index != busted else 2, trace_results[key][0])
542 self.assertEqual(actual_results[key]['output'], trace_results[key][1])
544 self.assertEqual(['output', 'results'], sorted(actual_results[key]))
545 results = actual_results[key]['results']
546 results = results.strip_root(unicode(ROOT_DIR))
547 actual = results.flatten()
548 self.assertTrue(actual['root'].pop('pid'))
550 self.assertTrue(actual['root']['children'][0].pop('pid'))
551 self.assertEqual(expected_results[index], actual)
553 if sys.platform != 'win32':
554 def test_trace_symlink(self):
560 os.path.join('trace_inputs', 'symlink.py'),
562 'executable': self.real_executable,
566 'path': os.path.join(REL_DATA, 'files2', 'bar'),
567 'size': self._size(REL_DATA, 'files2', 'bar'),
571 'path': os.path.join(REL_DATA, 'files2', 'foo'),
572 'size': self._size(REL_DATA, 'files2', 'foo'),
576 'path': os.path.join(REL_DATA, 'symlink.py'),
577 'size': self._size(REL_DATA, 'symlink.py'),
580 'initial_cwd': self.initial_cwd,
583 cmd = [sys.executable, os.path.join('trace_inputs', 'symlink.py')]
584 results = self._execute_trace(cmd)
585 actual = results.flatten()
586 self.assertTrue(actual['root'].pop('pid'))
587 self.assertEqual(expected, actual)
589 # In particular, the symlink is *not* resolved.
590 u'tests/trace_inputs/files2/'.replace('/', os.path.sep),
591 u'tests/trace_inputs/symlink.py'.replace('/', os.path.sep),
594 return f.endswith(('.pyc', '.svn', 'do_not_care.txt'))
595 simplified = trace_inputs.extract_directories(
596 unicode(ROOT_DIR), results.files, blacklist)
597 self.assertEqual(files, [f.path for f in simplified])
600 def test_trace_quoted(self):
601 results = self._execute_trace([sys.executable, '-c', 'print("hi")'])
610 'executable': self.real_executable,
612 'initial_cwd': self.initial_cwd,
615 actual = results.flatten()
616 self.assertTrue(actual['root'].pop('pid'))
617 self.assertEqual(expected, actual)
620 def _touch_expected(self, command):
621 # Looks for file that were touched but not opened, using different apis.
622 results = self._execute_trace(
623 [sys.executable, os.path.join('trace_inputs', 'touch_only.py'), command])
629 os.path.join('trace_inputs', 'touch_only.py'),
632 'executable': self.real_executable,
636 'path': os.path.join(REL_DATA, 'test_file.txt'),
637 'size': self._size(REL_DATA, 'test_file.txt'),
641 'path': os.path.join(REL_DATA, 'touch_only.py'),
642 'size': self._size(REL_DATA, 'touch_only.py'),
645 'initial_cwd': self.initial_cwd,
648 if sys.platform != 'linux2':
649 # TODO(maruel): Remove once properly implemented.
650 expected['root']['files'].pop(0)
652 actual = results.flatten()
653 self.assertTrue(actual['root'].pop('pid'))
654 self.assertEqual(expected, actual)
656 def test_trace_touch_only_access(self):
657 self._touch_expected('access')
659 def test_trace_touch_only_isfile(self):
660 self._touch_expected('isfile')
662 def test_trace_touch_only_stat(self):
663 self._touch_expected('stat')
666 def test_trace_tricky_filename(self):
667 # TODO(maruel): On Windows, it's using the current code page so some
668 # characters can't be represented. As a nice North American, hard code the
669 # string to something representable in code page 1252. The exact code page
670 # depends on the user system.
671 if sys.platform == 'win32':
672 filename = u'foo, bar, ~p#o,,ué^t%t .txt'
674 filename = u'foo, bar, ~p#o,,ué^t%t 和平.txt'
676 exe = os.path.join(self.tempdir, 'tricky_filename.py')
678 os.path.join(self.cwd, 'trace_inputs', 'tricky_filename.py'), exe)
686 'executable': self.real_executable,
691 'size': long(len('Bingo!')),
695 'path': u'tricky_filename.py',
696 'size': self._size(REL_DATA, 'tricky_filename.py'),
699 'initial_cwd': self.tempdir if sys.platform != 'win32' else None,
703 api = trace_inputs.get_api()
704 returncode, output = trace_inputs.trace(
705 self.log, [exe], self.tempdir, api, True)
706 self.assertEqual('', output)
707 self.assertEqual(0, returncode)
708 data = api.parse_log(self.log, lambda _: False, None)
709 self.assertEqual(1, len(data))
710 if 'exception' in data[0]:
711 raise data[0]['exception'][0], \
712 data[0]['exception'][1], \
713 data[0]['exception'][2]
714 actual = data[0]['results'].strip_root(self.tempdir).flatten()
715 self.assertTrue(actual['root'].pop('pid'))
716 self.assertEqual(expected, actual)
717 trace_inputs.get_api().clean_trace(self.log)
719 unicodedata.normalize('NFC', i)
720 for i in os.listdir(unicode(self.tempdir)))
721 self.assertEqual([filename, 'tricky_filename.py'], files)
724 if __name__ == '__main__':
725 VERBOSE = '-v' in sys.argv
726 logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR)
728 unittest.TestCase.maxDiff = None
729 # Necessary for the dtrace logger to work around execve() hook. See
730 # trace_inputs.py for more details.
731 os.environ['TRACE_INPUTS_DTRACE_ENABLE_EXECVE'] = '1'
732 print >> sys.stderr, 'Test are currently disabled'