Index: tests/trace_inputs_smoke_test.py |
diff --git a/tests/trace_inputs_smoke_test.py b/tests/trace_inputs_smoke_test.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..482be75c9ac837f143d8747a34ddb3bee7306101 |
--- /dev/null |
+++ b/tests/trace_inputs_smoke_test.py |
@@ -0,0 +1,614 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import json |
+import logging |
+import os |
+import shutil |
+import subprocess |
+import sys |
+import tempfile |
+import unittest |
+ |
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
+sys.path.insert(0, ROOT_DIR) |
+ |
+import run_test_cases |
+ |
+FILENAME = os.path.basename(__file__) |
+REL_DATA = os.path.join(u'tests', 'trace_inputs') |
+VERBOSE = False |
+ |
+ |
+class CalledProcessError(subprocess.CalledProcessError): |
+ """Makes 2.6 version act like 2.7""" |
+ def __init__(self, returncode, cmd, output, cwd): |
+ super(CalledProcessError, self).__init__(returncode, cmd) |
+ self.output = output |
+ self.cwd = cwd |
+ |
+ def __str__(self): |
+ return super(CalledProcessError, self).__str__() + ( |
+ '\n' |
+ 'cwd=%s\n%s') % (self.cwd, self.output) |
+ |
+ |
+class TraceInputsBase(unittest.TestCase): |
+ def setUp(self): |
+ self.tempdir = tempfile.mkdtemp(prefix='trace_smoke_test') |
+ self.log = os.path.join(self.tempdir, 'log') |
+ self.trace_inputs_path = os.path.join(ROOT_DIR, 'trace_inputs.py') |
+ |
+ # Wraps up all the differences between OSes here. |
+ # - Windows doesn't track initial_cwd. |
+ # - OSX replaces /usr/bin/python with /usr/bin/python2.7. |
+ self.cwd = os.path.join(ROOT_DIR, u'tests') |
+ self.initial_cwd = unicode(self.cwd) |
+ self.expected_cwd = unicode(ROOT_DIR) |
+ if sys.platform == 'win32': |
+ # Not supported on Windows. |
+ self.initial_cwd = None |
+ self.expected_cwd = None |
+ |
+ # There's 3 kinds of references to python, self.executable, |
+ # self.real_executable and self.naked_executable. It depends how python was |
+ # started. |
+ self.executable = sys.executable |
+ if sys.platform == 'darwin': |
+ # /usr/bin/python is a thunk executable that decides which version of |
+ # python gets executed. |
+ suffix = '.'.join(map(str, sys.version_info[0:2])) |
+ if os.access(self.executable + suffix, os.X_OK): |
+ # So it'll look like /usr/bin/python2.7 |
+ self.executable += suffix |
+ |
+ import trace_inputs |
+ self.real_executable = trace_inputs.get_native_path_case( |
+ unicode(self.executable)) |
+ trace_inputs = None |
+ |
+ # self.naked_executable will only be naked on Windows. |
+ self.naked_executable = unicode(sys.executable) |
+ if sys.platform == 'win32': |
+ self.naked_executable = os.path.basename(sys.executable) |
+ |
+ def tearDown(self): |
+ if VERBOSE: |
+ print 'Leaking: %s' % self.tempdir |
+ else: |
+ shutil.rmtree(self.tempdir) |
+ |
+ @staticmethod |
+ def get_child_command(from_data): |
+ """Returns command to run the child1.py.""" |
+ cmd = [sys.executable] |
+ if from_data: |
+ # When the gyp argument is specified, the command is started from --cwd |
+ # directory. In this case, 'tests'. |
+ cmd.extend([os.path.join('trace_inputs', 'child1.py'), '--child-gyp']) |
+ else: |
+ # When the gyp argument is not specified, the command is started from |
+ # --root-dir directory. |
+ cmd.extend([os.path.join(REL_DATA, 'child1.py'), '--child']) |
+ return cmd |
+ |
+ @staticmethod |
+ def _size(*args): |
+ return os.stat(os.path.join(ROOT_DIR, *args)).st_size |
+ |
+ |
+class TraceInputs(TraceInputsBase): |
+ def _execute(self, mode, command, cwd): |
+ cmd = [ |
+ sys.executable, |
+ self.trace_inputs_path, |
+ mode, |
+ '--log', self.log, |
+ ] |
+ if VERBOSE: |
+ cmd.extend(['-v'] * 3) |
+ cmd.extend(command) |
+ logging.info('Command: %s' % ' '.join(cmd)) |
+ p = subprocess.Popen( |
+ cmd, |
+ stdout=subprocess.PIPE, |
+ stderr=subprocess.PIPE, |
+ cwd=cwd, |
+ universal_newlines=True) |
+ out, err = p.communicate() |
+ if VERBOSE: |
+ print err |
+ if p.returncode: |
+ raise CalledProcessError(p.returncode, cmd, out + err, cwd) |
+ return out or '' |
+ |
+ def _trace(self, from_data): |
+ if from_data: |
+ cwd = os.path.join(ROOT_DIR, 'tests') |
+ else: |
+ cwd = ROOT_DIR |
+ return self._execute('trace', self.get_child_command(from_data), cwd=cwd) |
+ |
+ def test_trace(self): |
+ expected = '\n'.join(( |
+ 'Total: 7', |
+ 'Non existent: 0', |
+ 'Interesting: 7 reduced to 6', |
+ ' tests/trace_inputs/child1.py'.replace('/', os.path.sep), |
+ ' tests/trace_inputs/child2.py'.replace('/', os.path.sep), |
+ ' tests/trace_inputs/files1/'.replace('/', os.path.sep), |
+ ' tests/trace_inputs/test_file.txt'.replace('/', os.path.sep), |
+ (' tests/%s' % FILENAME).replace('/', os.path.sep), |
+ ' trace_inputs.py', |
+ )) + '\n' |
+ trace_expected = '\n'.join(( |
+ 'child from %s' % ROOT_DIR, |
+ 'child2', |
+ )) + '\n' |
+ trace_actual = self._trace(False) |
+ actual = self._execute( |
+ 'read', |
+ [ |
+ '--root-dir', ROOT_DIR, |
+ '--blacklist', '.+\\.pyc', |
+ '--blacklist', '.*\\.svn', |
+ '--blacklist', '.*do_not_care\\.txt', |
+ ], |
+ cwd=ROOT_DIR) |
+ self.assertEquals(expected, actual) |
+ self.assertEquals(trace_expected, trace_actual) |
+ |
+ def test_trace_json(self): |
+ expected = { |
+ u'root': { |
+ u'children': [ |
+ { |
+ u'children': [], |
+ u'command': [u'python', u'child2.py'], |
+ u'executable': self.naked_executable, |
+ u'files': [ |
+ { |
+ u'path': os.path.join(REL_DATA, 'child2.py'), |
+ u'size': self._size(REL_DATA, 'child2.py'), |
+ }, |
+ { |
+ u'path': os.path.join(REL_DATA, 'files1', 'bar'), |
+ u'size': self._size(REL_DATA, 'files1', 'bar'), |
+ }, |
+ { |
+ u'path': os.path.join(REL_DATA, 'files1', 'foo'), |
+ u'size': self._size(REL_DATA, 'files1', 'foo'), |
+ }, |
+ { |
+ u'path': os.path.join(REL_DATA, 'test_file.txt'), |
+ u'size': self._size(REL_DATA, 'test_file.txt'), |
+ }, |
+ ], |
+ u'initial_cwd': self.initial_cwd, |
+ #u'pid': 123, |
+ }, |
+ ], |
+ u'command': [ |
+ unicode(self.executable), |
+ os.path.join(u'trace_inputs', 'child1.py'), |
+ u'--child-gyp', |
+ ], |
+ u'executable': self.real_executable, |
+ u'files': [ |
+ { |
+ u'path': os.path.join(REL_DATA, 'child1.py'), |
+ u'size': self._size(REL_DATA, 'child1.py'), |
+ }, |
+ { |
+ u'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), |
+ u'size': self._size('tests', 'trace_inputs_smoke_test.py'), |
+ }, |
+ { |
+ u'path': u'trace_inputs.py', |
+ u'size': self._size('trace_inputs.py'), |
+ }, |
+ ], |
+ u'initial_cwd': self.initial_cwd, |
+ #u'pid': 123, |
+ }, |
+ } |
+ trace_expected = '\n'.join(( |
+ 'child_gyp from %s' % os.path.join(ROOT_DIR, 'tests'), |
+ 'child2', |
+ )) + '\n' |
+ trace_actual = self._trace(True) |
+ actual_text = self._execute( |
+ 'read', |
+ [ |
+ '--root-dir', ROOT_DIR, |
+ '--blacklist', '.+\\.pyc', |
+ '--blacklist', '.*\\.svn', |
+ '--blacklist', '.*do_not_care\\.txt', |
+ '--json', |
+ ], |
+ cwd=ROOT_DIR) |
+ actual_json = json.loads(actual_text) |
+ self.assertEquals(list, actual_json.__class__) |
+ self.assertEquals(1, len(actual_json)) |
+ actual_json = actual_json[0] |
+ # Removes the pids. |
+ self.assertTrue(actual_json['root'].pop('pid')) |
+ self.assertTrue(actual_json['root']['children'][0].pop('pid')) |
+ self.assertEquals(expected, actual_json) |
+ self.assertEquals(trace_expected, trace_actual) |
+ |
+ |
+class TraceInputsImport(TraceInputsBase): |
+ def setUp(self): |
+ super(TraceInputsImport, self).setUp() |
+ import trace_inputs |
+ self.trace_inputs = trace_inputs |
+ |
+ def tearDown(self): |
+ del self.trace_inputs |
+ super(TraceInputsImport, self).tearDown() |
+ |
+ # Similar to TraceInputs test fixture except that it calls the function |
+ # directly, so the Results instance can be inspected. |
+ # Roughly, make sure the API is stable. |
+ def _execute_trace(self, command): |
+ # Similar to what trace_test_cases.py does. |
+ api = self.trace_inputs.get_api() |
+ _, _ = self.trace_inputs.trace( |
+ self.log, command, self.cwd, api, True) |
+ # TODO(maruel): Check |
+ #self.assertEquals(0, returncode) |
+ #self.assertEquals('', output) |
+ def blacklist(f): |
+ return f.endswith(('.pyc', '.svn', 'do_not_care.txt')) |
+ return self.trace_inputs.load_trace(self.log, ROOT_DIR, api, blacklist) |
+ |
+ def _gen_dict_wrong_path(self): |
+ """Returns the expected flattened Results when child1.py is called with the |
+ wrong relative path. |
+ """ |
+ return { |
+ 'root': { |
+ 'children': [], |
+ 'command': [ |
+ self.executable, |
+ os.path.join(REL_DATA, 'child1.py'), |
+ '--child', |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ } |
+ |
+ def _gen_dict_full(self): |
+ """Returns the expected flattened Results when child1.py is called with |
+ --child. |
+ """ |
+ return { |
+ 'root': { |
+ 'children': [ |
+ { |
+ 'children': [], |
+ 'command': ['python', 'child2.py'], |
+ 'executable': self.naked_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'child2.py'), |
+ 'size': self._size(REL_DATA, 'child2.py'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'files1', 'bar'), |
+ 'size': self._size(REL_DATA, 'files1', 'bar'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'files1', 'foo'), |
+ 'size': self._size(REL_DATA, 'files1', 'foo'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'test_file.txt'), |
+ 'size': self._size(REL_DATA, 'test_file.txt'), |
+ }, |
+ ], |
+ 'initial_cwd': self.expected_cwd, |
+ }, |
+ ], |
+ 'command': [ |
+ self.executable, |
+ os.path.join(REL_DATA, 'child1.py'), |
+ '--child', |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'child1.py'), |
+ 'size': self._size(REL_DATA, 'child1.py'), |
+ }, |
+ { |
+ u'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), |
+ 'size': self._size('tests', 'trace_inputs_smoke_test.py'), |
+ }, |
+ { |
+ 'path': u'trace_inputs.py', |
+ 'size': self._size('trace_inputs.py'), |
+ }, |
+ ], |
+ 'initial_cwd': self.expected_cwd, |
+ }, |
+ } |
+ |
+ def _gen_dict_full_gyp(self): |
+ """Returns the expected flattened results when child1.py is called with |
+ --child-gyp. |
+ """ |
+ return { |
+ 'root': { |
+ 'children': [ |
+ { |
+ 'children': [], |
+ 'command': ['python', 'child2.py'], |
+ 'executable': self.naked_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'child2.py'), |
+ 'size': self._size(REL_DATA, 'child2.py'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'files1', 'bar'), |
+ 'size': self._size(REL_DATA, 'files1', 'bar'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'files1', 'foo'), |
+ 'size': self._size(REL_DATA, 'files1', 'foo'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'test_file.txt'), |
+ 'size': self._size(REL_DATA, 'test_file.txt'), |
+ }, |
+ ], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ ], |
+ 'command': [ |
+ self.executable, |
+ os.path.join('trace_inputs', 'child1.py'), |
+ '--child-gyp', |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'child1.py'), |
+ 'size': self._size(REL_DATA, 'child1.py'), |
+ }, |
+ { |
+ 'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), |
+ 'size': self._size('tests', 'trace_inputs_smoke_test.py'), |
+ }, |
+ { |
+ 'path': u'trace_inputs.py', |
+ 'size': self._size('trace_inputs.py'), |
+ }, |
+ ], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ } |
+ |
+ def test_trace_wrong_path(self): |
+ # Deliberately start the trace from the wrong path. Starts it from the |
+ # directory 'tests' so 'tests/tests/trace_inputs/child1.py' is not |
+ # accessible, so child2.py process is not started. |
+ results = self._execute_trace(self.get_child_command(False)) |
+ expected = self._gen_dict_wrong_path() |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ self.assertEquals(expected, actual) |
+ |
+ def test_trace(self): |
+ expected = self._gen_dict_full_gyp() |
+ results = self._execute_trace(self.get_child_command(True)) |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ self.assertTrue(actual['root']['children'][0].pop('pid')) |
+ self.assertEquals(expected, actual) |
+ files = [ |
+ u'tests/trace_inputs/child1.py'.replace('/', os.path.sep), |
+ u'tests/trace_inputs/child2.py'.replace('/', os.path.sep), |
+ u'tests/trace_inputs/files1/'.replace('/', os.path.sep), |
+ u'tests/trace_inputs/test_file.txt'.replace('/', os.path.sep), |
+ u'tests/trace_inputs_smoke_test.py'.replace('/', os.path.sep), |
+ u'trace_inputs.py', |
+ ] |
+ def blacklist(f): |
+ return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn')) |
+ simplified = self.trace_inputs.extract_directories( |
+ ROOT_DIR, results.files, blacklist) |
+ self.assertEquals(files, [f.path for f in simplified]) |
+ |
+ def test_trace_multiple(self): |
+ # Starts parallel threads and trace parallel child processes simultaneously. |
+ # Some are started from 'tests' directory, others from this script's |
+ # directory. One trace fails. Verify everything still goes one. |
+ parallel = 8 |
+ |
+ def trace(tracer, cmd, cwd, tracename): |
+ resultcode, output = tracer.trace( |
+ cmd, cwd, tracename, True) |
+ return (tracename, resultcode, output) |
+ |
+ with run_test_cases.ThreadPool(parallel) as pool: |
+ api = self.trace_inputs.get_api() |
+ with api.get_tracer(self.log) as tracer: |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace1') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(True), self.cwd, 'trace2') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace3') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(True), self.cwd, 'trace4') |
+ # Have this one fail since it's started from the wrong directory. |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(False), self.cwd, 'trace5') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(True), self.cwd, 'trace6') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace7') |
+ pool.add_task( |
+ trace, tracer, self.get_child_command(True), self.cwd, 'trace8') |
+ trace_results = pool.join() |
+ def blacklist(f): |
+ return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn')) |
+ actual_results = api.parse_log(self.log, blacklist) |
+ self.assertEquals(8, len(trace_results)) |
+ self.assertEquals(8, len(actual_results)) |
+ |
+ # Convert to dict keyed on the trace name, simpler to verify. |
+ trace_results = dict((i[0], i[1:]) for i in trace_results) |
+ actual_results = dict((x.pop('trace'), x) for x in actual_results) |
+ self.assertEquals(sorted(trace_results), sorted(actual_results)) |
+ |
+ # It'd be nice to start different kinds of processes. |
+ expected_results = [ |
+ self._gen_dict_full(), |
+ self._gen_dict_full_gyp(), |
+ self._gen_dict_full(), |
+ self._gen_dict_full_gyp(), |
+ self._gen_dict_wrong_path(), |
+ self._gen_dict_full_gyp(), |
+ self._gen_dict_full(), |
+ self._gen_dict_full_gyp(), |
+ ] |
+ self.assertEquals(len(expected_results), len(trace_results)) |
+ |
+ # See the comment above about the trace that fails because it's started from |
+ # the wrong directory. |
+ busted = 4 |
+ for index, key in enumerate(sorted(actual_results)): |
+ self.assertEquals('trace%d' % (index + 1), key) |
+ self.assertEquals(2, len(trace_results[key])) |
+ # returncode |
+ self.assertEquals(0 if index != busted else 2, trace_results[key][0]) |
+ # output |
+ self.assertEquals(actual_results[key]['output'], trace_results[key][1]) |
+ |
+ self.assertEquals(['output', 'results'], sorted(actual_results[key])) |
+ results = actual_results[key]['results'] |
+ results = results.strip_root(ROOT_DIR) |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ if index != busted: |
+ self.assertTrue(actual['root']['children'][0].pop('pid')) |
+ self.assertEquals(expected_results[index], actual) |
+ |
+ if sys.platform != 'win32': |
+ def test_trace_symlink(self): |
+ expected = { |
+ 'root': { |
+ 'children': [], |
+ 'command': [ |
+ self.executable, |
+ os.path.join('trace_inputs', 'symlink.py'), |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'files2', 'bar'), |
+ 'size': self._size(REL_DATA, 'files2', 'bar'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'files2', 'foo'), |
+ 'size': self._size(REL_DATA, 'files2', 'foo'), |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'symlink.py'), |
+ 'size': self._size(REL_DATA, 'symlink.py'), |
+ }, |
+ ], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ } |
+ cmd = [sys.executable, os.path.join('trace_inputs', 'symlink.py')] |
+ results = self._execute_trace(cmd) |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ self.assertEquals(expected, actual) |
+ files = [ |
+ # In particular, the symlink is *not* resolved. |
+ u'tests/trace_inputs/files2/'.replace('/', os.path.sep), |
+ u'tests/trace_inputs/symlink.py'.replace('/', os.path.sep), |
+ ] |
+ def blacklist(f): |
+ return f.endswith(('.pyc', '.svn', 'do_not_care.txt')) |
+ simplified = self.trace_inputs.extract_directories( |
+ ROOT_DIR, results.files, blacklist) |
+ self.assertEquals(files, [f.path for f in simplified]) |
+ |
+ def test_trace_quoted(self): |
+ results = self._execute_trace([sys.executable, '-c', 'print("hi")']) |
+ expected = { |
+ 'root': { |
+ 'children': [], |
+ 'command': [ |
+ self.executable, |
+ '-c', |
+ 'print("hi")', |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ } |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ self.assertEquals(expected, actual) |
+ |
+ def _touch_expected(self, command): |
+ # Looks for file that were touched but not opened, using different apis. |
+ results = self._execute_trace( |
+ [sys.executable, os.path.join('trace_inputs', 'touch_only.py'), command]) |
+ expected = { |
+ 'root': { |
+ 'children': [], |
+ 'command': [ |
+ self.executable, |
+ os.path.join('trace_inputs', 'touch_only.py'), |
+ command, |
+ ], |
+ 'executable': self.real_executable, |
+ 'files': [ |
+ { |
+ 'path': os.path.join(REL_DATA, 'test_file.txt'), |
+ 'size': 0, |
+ }, |
+ { |
+ 'path': os.path.join(REL_DATA, 'touch_only.py'), |
+ 'size': self._size(REL_DATA, 'touch_only.py'), |
+ }, |
+ ], |
+ 'initial_cwd': self.initial_cwd, |
+ }, |
+ } |
+ if sys.platform != 'linux2': |
+ # TODO(maruel): Remove once properly implemented. |
+ expected['root']['files'].pop(0) |
+ |
+ actual = results.flatten() |
+ self.assertTrue(actual['root'].pop('pid')) |
+ self.assertEquals(expected, actual) |
+ |
+ def test_trace_touch_only_access(self): |
+ self._touch_expected('access') |
+ |
+ def test_trace_touch_only_isfile(self): |
+ self._touch_expected('isfile') |
+ |
+ def test_trace_touch_only_stat(self): |
+ self._touch_expected('stat') |
+ |
+ |
+if __name__ == '__main__': |
+ VERBOSE = '-v' in sys.argv |
+ logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR) |
+ unittest.main() |