| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # coding=utf-8 | |
| 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 4 # Use of this source code is governed by a BSD-style license that can be | |
| 5 # found in the LICENSE file. | |
| 6 | |
| 7 import json | |
| 8 import logging | |
| 9 import os | |
| 10 import shutil | |
| 11 import subprocess | |
| 12 import sys | |
| 13 import tempfile | |
| 14 import unicodedata | |
| 15 import unittest | |
| 16 | |
| 17 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| 18 sys.path.insert(0, ROOT_DIR) | |
| 19 | |
| 20 import trace_inputs | |
| 21 from utils import file_path | |
| 22 from utils import threading_utils | |
| 23 | |
| 24 FILENAME = os.path.basename(__file__) | |
| 25 REL_DATA = os.path.join(u'tests', 'trace_inputs') | |
| 26 VERBOSE = False | |
| 27 | |
| 28 # TODO(maruel): Have the kernel tracer on Windows differentiate between file | |
| 29 # read or file write. | |
| 30 MODE_R = trace_inputs.Results.File.READ if sys.platform != 'win32' else None | |
| 31 MODE_W = trace_inputs.Results.File.WRITE if sys.platform != 'win32' else None | |
| 32 MODE_T = trace_inputs.Results.File.TOUCHED | |
| 33 | |
| 34 | |
| 35 class CalledProcessError(subprocess.CalledProcessError): | |
| 36 """Makes 2.6 version act like 2.7""" | |
| 37 def __init__(self, returncode, cmd, output, cwd): | |
| 38 super(CalledProcessError, self).__init__(returncode, cmd) | |
| 39 self.output = output | |
| 40 self.cwd = cwd | |
| 41 | |
| 42 def __str__(self): | |
| 43 return super(CalledProcessError, self).__str__() + ( | |
| 44 '\n' | |
| 45 'cwd=%s\n%s') % (self.cwd, self.output) | |
| 46 | |
| 47 | |
| 48 class TraceInputsBase(unittest.TestCase): | |
| 49 def setUp(self): | |
| 50 self.tempdir = None | |
| 51 self.trace_inputs_path = os.path.join(ROOT_DIR, 'trace_inputs.py') | |
| 52 | |
| 53 # Wraps up all the differences between OSes here. | |
| 54 # - Windows doesn't track initial_cwd. | |
| 55 # - OSX replaces /usr/bin/python with /usr/bin/python2.7. | |
| 56 self.cwd = os.path.join(ROOT_DIR, u'tests') | |
| 57 self.initial_cwd = unicode(self.cwd) | |
| 58 self.expected_cwd = unicode(ROOT_DIR) | |
| 59 if sys.platform == 'win32': | |
| 60 # Not supported on Windows. | |
| 61 self.initial_cwd = None | |
| 62 self.expected_cwd = None | |
| 63 | |
| 64 # There's 3 kinds of references to python, self.executable, | |
| 65 # self.real_executable and self.naked_executable. It depends how python was | |
| 66 # started. | |
| 67 self.executable = sys.executable | |
| 68 if sys.platform == 'darwin': | |
| 69 # /usr/bin/python is a thunk executable that decides which version of | |
| 70 # python gets executed. | |
| 71 suffix = '.'.join(map(str, sys.version_info[0:2])) | |
| 72 if os.access(self.executable + suffix, os.X_OK): | |
| 73 # So it'll look like /usr/bin/python2.7 | |
| 74 self.executable += suffix | |
| 75 | |
| 76 self.real_executable = file_path.get_native_path_case( | |
| 77 unicode(self.executable)) | |
| 78 self.tempdir = file_path.get_native_path_case( | |
| 79 unicode(tempfile.mkdtemp(prefix='trace_smoke_test'))) | |
| 80 self.log = os.path.join(self.tempdir, 'log') | |
| 81 | |
| 82 # self.naked_executable will only be naked on Windows. | |
| 83 self.naked_executable = unicode(sys.executable) | |
| 84 if sys.platform == 'win32': | |
| 85 self.naked_executable = os.path.basename(sys.executable) | |
| 86 | |
| 87 def tearDown(self): | |
| 88 if self.tempdir: | |
| 89 if VERBOSE: | |
| 90 print 'Leaking: %s' % self.tempdir | |
| 91 else: | |
| 92 shutil.rmtree(self.tempdir) | |
| 93 | |
| 94 @staticmethod | |
| 95 def get_child_command(from_data): | |
| 96 """Returns command to run the child1.py.""" | |
| 97 cmd = [sys.executable] | |
| 98 if from_data: | |
| 99 # When the gyp argument is specified, the command is started from --cwd | |
| 100 # directory. In this case, 'tests'. | |
| 101 cmd.extend([os.path.join('trace_inputs', 'child1.py'), '--child-gyp']) | |
| 102 else: | |
| 103 # When the gyp argument is not specified, the command is started from | |
| 104 # --root-dir directory. | |
| 105 cmd.extend([os.path.join(REL_DATA, 'child1.py'), '--child']) | |
| 106 return cmd | |
| 107 | |
| 108 @staticmethod | |
| 109 def _size(*args): | |
| 110 return os.stat(os.path.join(ROOT_DIR, *args)).st_size | |
| 111 | |
| 112 | |
| 113 class TraceInputs(TraceInputsBase): | |
| 114 def _execute(self, mode, command, cwd): | |
| 115 cmd = [ | |
| 116 sys.executable, | |
| 117 self.trace_inputs_path, | |
| 118 mode, | |
| 119 '--log', self.log, | |
| 120 ] | |
| 121 if VERBOSE: | |
| 122 cmd.extend(['-v'] * 3) | |
| 123 cmd.extend(command) | |
| 124 logging.info('Command: %s' % ' '.join(cmd)) | |
| 125 p = subprocess.Popen( | |
| 126 cmd, | |
| 127 stdout=subprocess.PIPE, | |
| 128 stderr=subprocess.PIPE, | |
| 129 cwd=cwd, | |
| 130 universal_newlines=True) | |
| 131 out, err = p.communicate() | |
| 132 if VERBOSE: | |
| 133 print err | |
| 134 if p.returncode: | |
| 135 raise CalledProcessError(p.returncode, cmd, out + err, cwd) | |
| 136 return out or '' | |
| 137 | |
| 138 def _trace(self, from_data): | |
| 139 if from_data: | |
| 140 cwd = os.path.join(ROOT_DIR, 'tests') | |
| 141 else: | |
| 142 cwd = ROOT_DIR | |
| 143 return self._execute('trace', self.get_child_command(from_data), cwd=cwd) | |
| 144 | |
| 145 def test_trace(self): | |
| 146 expected = '\n'.join(( | |
| 147 'Total: 7', | |
| 148 'Non existent: 0', | |
| 149 'Interesting: 7 reduced to 6', | |
| 150 ' tests/trace_inputs/child1.py'.replace('/', os.path.sep), | |
| 151 ' tests/trace_inputs/child2.py'.replace('/', os.path.sep), | |
| 152 ' tests/trace_inputs/files1/'.replace('/', os.path.sep), | |
| 153 ' tests/trace_inputs/test_file.txt'.replace('/', os.path.sep), | |
| 154 (' tests/%s' % FILENAME).replace('/', os.path.sep), | |
| 155 ' trace_inputs.py', | |
| 156 )) + '\n' | |
| 157 trace_expected = '\n'.join(( | |
| 158 'child from %s' % ROOT_DIR, | |
| 159 'child2', | |
| 160 )) + '\n' | |
| 161 trace_actual = self._trace(False) | |
| 162 actual = self._execute( | |
| 163 'read', | |
| 164 [ | |
| 165 '--root-dir', ROOT_DIR, | |
| 166 '--trace-blacklist', '.+\\.pyc', | |
| 167 '--trace-blacklist', '.*\\.svn', | |
| 168 '--trace-blacklist', '.*do_not_care\\.txt', | |
| 169 ], | |
| 170 cwd=unicode(ROOT_DIR)) | |
| 171 self.assertEqual(expected, actual) | |
| 172 self.assertEqual(trace_expected, trace_actual) | |
| 173 | |
| 174 def test_trace_json(self): | |
| 175 expected = { | |
| 176 u'root': { | |
| 177 u'children': [ | |
| 178 { | |
| 179 u'children': [], | |
| 180 u'command': [u'python', u'child2.py'], | |
| 181 u'executable': self.naked_executable, | |
| 182 u'files': [ | |
| 183 { | |
| 184 'mode': MODE_R, | |
| 185 u'path': os.path.join(REL_DATA, 'child2.py'), | |
| 186 u'size': self._size(REL_DATA, 'child2.py'), | |
| 187 }, | |
| 188 { | |
| 189 'mode': MODE_R, | |
| 190 u'path': os.path.join(REL_DATA, 'files1', 'bar'), | |
| 191 u'size': self._size(REL_DATA, 'files1', 'bar'), | |
| 192 }, | |
| 193 { | |
| 194 'mode': MODE_R, | |
| 195 u'path': os.path.join(REL_DATA, 'files1', 'foo'), | |
| 196 u'size': self._size(REL_DATA, 'files1', 'foo'), | |
| 197 }, | |
| 198 { | |
| 199 'mode': MODE_R, | |
| 200 u'path': os.path.join(REL_DATA, 'test_file.txt'), | |
| 201 u'size': self._size(REL_DATA, 'test_file.txt'), | |
| 202 }, | |
| 203 ], | |
| 204 u'initial_cwd': self.initial_cwd, | |
| 205 #u'pid': 123, | |
| 206 }, | |
| 207 ], | |
| 208 u'command': [ | |
| 209 unicode(self.executable), | |
| 210 os.path.join(u'trace_inputs', 'child1.py'), | |
| 211 u'--child-gyp', | |
| 212 ], | |
| 213 u'executable': self.real_executable, | |
| 214 u'files': [ | |
| 215 { | |
| 216 u'mode': MODE_R, | |
| 217 u'path': os.path.join(REL_DATA, 'child1.py'), | |
| 218 u'size': self._size(REL_DATA, 'child1.py'), | |
| 219 }, | |
| 220 { | |
| 221 u'mode': MODE_R, | |
| 222 u'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), | |
| 223 u'size': self._size('tests', 'trace_inputs_smoke_test.py'), | |
| 224 }, | |
| 225 { | |
| 226 u'mode': MODE_R, | |
| 227 u'path': u'trace_inputs.py', | |
| 228 u'size': self._size('trace_inputs.py'), | |
| 229 }, | |
| 230 ], | |
| 231 u'initial_cwd': self.initial_cwd, | |
| 232 #u'pid': 123, | |
| 233 }, | |
| 234 } | |
| 235 trace_expected = '\n'.join(( | |
| 236 'child_gyp from %s' % os.path.join(ROOT_DIR, 'tests'), | |
| 237 'child2', | |
| 238 )) + '\n' | |
| 239 trace_actual = self._trace(True) | |
| 240 actual_text = self._execute( | |
| 241 'read', | |
| 242 [ | |
| 243 '--root-dir', ROOT_DIR, | |
| 244 '--trace-blacklist', '.+\\.pyc', | |
| 245 '--trace-blacklist', '.*\\.svn', | |
| 246 '--trace-blacklist', '.*do_not_care\\.txt', | |
| 247 '--json', | |
| 248 ], | |
| 249 cwd=unicode(ROOT_DIR)) | |
| 250 actual_json = json.loads(actual_text) | |
| 251 self.assertEqual(list, actual_json.__class__) | |
| 252 self.assertEqual(1, len(actual_json)) | |
| 253 actual_json = actual_json[0] | |
| 254 # Removes the pids. | |
| 255 self.assertTrue(actual_json['root'].pop('pid')) | |
| 256 self.assertTrue(actual_json['root']['children'][0].pop('pid')) | |
| 257 self.assertEqual(expected, actual_json) | |
| 258 self.assertEqual(trace_expected, trace_actual) | |
| 259 | |
| 260 | |
| 261 class TraceInputsImport(TraceInputsBase): | |
| 262 | |
| 263 # Similar to TraceInputs test fixture except that it calls the function | |
| 264 # directly, so the Results instance can be inspected. | |
| 265 # Roughly, make sure the API is stable. | |
| 266 def _execute_trace(self, command): | |
| 267 # Similar to what trace_test_cases.py does. | |
| 268 api = trace_inputs.get_api() | |
| 269 _, _ = trace_inputs.trace(self.log, command, self.cwd, api, True) | |
| 270 # TODO(maruel): Check | |
| 271 #self.assertEqual(0, returncode) | |
| 272 #self.assertEqual('', output) | |
| 273 def blacklist(f): | |
| 274 return f.endswith(('.pyc', '.svn', 'do_not_care.txt')) | |
| 275 data = api.parse_log(self.log, blacklist, None) | |
| 276 self.assertEqual(1, len(data)) | |
| 277 if 'exception' in data[0]: | |
| 278 raise data[0]['exception'][0], \ | |
| 279 data[0]['exception'][1], \ | |
| 280 data[0]['exception'][2] | |
| 281 | |
| 282 return data[0]['results'].strip_root(unicode(ROOT_DIR)) | |
| 283 | |
| 284 def _gen_dict_wrong_path(self): | |
| 285 """Returns the expected flattened Results when child1.py is called with the | |
| 286 wrong relative path. | |
| 287 """ | |
| 288 return { | |
| 289 'root': { | |
| 290 'children': [], | |
| 291 'command': [ | |
| 292 self.executable, | |
| 293 os.path.join(REL_DATA, 'child1.py'), | |
| 294 '--child', | |
| 295 ], | |
| 296 'executable': self.real_executable, | |
| 297 'files': [], | |
| 298 'initial_cwd': self.initial_cwd, | |
| 299 }, | |
| 300 } | |
| 301 | |
| 302 def _gen_dict_full(self): | |
| 303 """Returns the expected flattened Results when child1.py is called with | |
| 304 --child. | |
| 305 """ | |
| 306 return { | |
| 307 'root': { | |
| 308 'children': [ | |
| 309 { | |
| 310 'children': [], | |
| 311 'command': ['python', 'child2.py'], | |
| 312 'executable': self.naked_executable, | |
| 313 'files': [ | |
| 314 { | |
| 315 'mode': MODE_R, | |
| 316 'path': os.path.join(REL_DATA, 'child2.py'), | |
| 317 'size': self._size(REL_DATA, 'child2.py'), | |
| 318 }, | |
| 319 { | |
| 320 'mode': MODE_R, | |
| 321 'path': os.path.join(REL_DATA, 'files1', 'bar'), | |
| 322 'size': self._size(REL_DATA, 'files1', 'bar'), | |
| 323 }, | |
| 324 { | |
| 325 'mode': MODE_R, | |
| 326 'path': os.path.join(REL_DATA, 'files1', 'foo'), | |
| 327 'size': self._size(REL_DATA, 'files1', 'foo'), | |
| 328 }, | |
| 329 { | |
| 330 'mode': MODE_R, | |
| 331 'path': os.path.join(REL_DATA, 'test_file.txt'), | |
| 332 'size': self._size(REL_DATA, 'test_file.txt'), | |
| 333 }, | |
| 334 ], | |
| 335 'initial_cwd': self.expected_cwd, | |
| 336 }, | |
| 337 ], | |
| 338 'command': [ | |
| 339 self.executable, | |
| 340 os.path.join(REL_DATA, 'child1.py'), | |
| 341 '--child', | |
| 342 ], | |
| 343 'executable': self.real_executable, | |
| 344 'files': [ | |
| 345 { | |
| 346 'mode': MODE_R, | |
| 347 'path': os.path.join(REL_DATA, 'child1.py'), | |
| 348 'size': self._size(REL_DATA, 'child1.py'), | |
| 349 }, | |
| 350 { | |
| 351 'mode': MODE_R, | |
| 352 'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), | |
| 353 'size': self._size('tests', 'trace_inputs_smoke_test.py'), | |
| 354 }, | |
| 355 { | |
| 356 'mode': MODE_R, | |
| 357 'path': u'trace_inputs.py', | |
| 358 'size': self._size('trace_inputs.py'), | |
| 359 }, | |
| 360 ], | |
| 361 'initial_cwd': self.expected_cwd, | |
| 362 }, | |
| 363 } | |
| 364 | |
| 365 def _gen_dict_full_gyp(self): | |
| 366 """Returns the expected flattened results when child1.py is called with | |
| 367 --child-gyp. | |
| 368 """ | |
| 369 return { | |
| 370 'root': { | |
| 371 'children': [ | |
| 372 { | |
| 373 'children': [], | |
| 374 'command': [u'python', u'child2.py'], | |
| 375 'executable': self.naked_executable, | |
| 376 'files': [ | |
| 377 { | |
| 378 'mode': MODE_R, | |
| 379 'path': os.path.join(REL_DATA, 'child2.py'), | |
| 380 'size': self._size(REL_DATA, 'child2.py'), | |
| 381 }, | |
| 382 { | |
| 383 'mode': MODE_R, | |
| 384 'path': os.path.join(REL_DATA, 'files1', 'bar'), | |
| 385 'size': self._size(REL_DATA, 'files1', 'bar'), | |
| 386 }, | |
| 387 { | |
| 388 'mode': MODE_R, | |
| 389 'path': os.path.join(REL_DATA, 'files1', 'foo'), | |
| 390 'size': self._size(REL_DATA, 'files1', 'foo'), | |
| 391 }, | |
| 392 { | |
| 393 'mode': MODE_R, | |
| 394 'path': os.path.join(REL_DATA, 'test_file.txt'), | |
| 395 'size': self._size(REL_DATA, 'test_file.txt'), | |
| 396 }, | |
| 397 ], | |
| 398 'initial_cwd': self.initial_cwd, | |
| 399 }, | |
| 400 ], | |
| 401 'command': [ | |
| 402 self.executable, | |
| 403 os.path.join('trace_inputs', 'child1.py'), | |
| 404 '--child-gyp', | |
| 405 ], | |
| 406 'executable': self.real_executable, | |
| 407 'files': [ | |
| 408 { | |
| 409 'mode': MODE_R, | |
| 410 'path': os.path.join(REL_DATA, 'child1.py'), | |
| 411 'size': self._size(REL_DATA, 'child1.py'), | |
| 412 }, | |
| 413 { | |
| 414 'mode': MODE_R, | |
| 415 'path': os.path.join(u'tests', u'trace_inputs_smoke_test.py'), | |
| 416 'size': self._size('tests', 'trace_inputs_smoke_test.py'), | |
| 417 }, | |
| 418 { | |
| 419 'mode': MODE_R, | |
| 420 'path': u'trace_inputs.py', | |
| 421 'size': self._size('trace_inputs.py'), | |
| 422 }, | |
| 423 ], | |
| 424 'initial_cwd': self.initial_cwd, | |
| 425 }, | |
| 426 } | |
| 427 | |
| 428 def test_trace_wrong_path(self): | |
| 429 # Deliberately start the trace from the wrong path. Starts it from the | |
| 430 # directory 'tests' so 'tests/tests/trace_inputs/child1.py' is not | |
| 431 # accessible, so child2.py process is not started. | |
| 432 results = self._execute_trace(self.get_child_command(False)) | |
| 433 expected = self._gen_dict_wrong_path() | |
| 434 actual = results.flatten() | |
| 435 self.assertTrue(actual['root'].pop('pid')) | |
| 436 self.assertEqual(expected, actual) | |
| 437 | |
| 438 def test_trace(self): | |
| 439 expected = self._gen_dict_full_gyp() | |
| 440 results = self._execute_trace(self.get_child_command(True)) | |
| 441 actual = results.flatten() | |
| 442 self.assertTrue(actual['root'].pop('pid')) | |
| 443 self.assertTrue(actual['root']['children'][0].pop('pid')) | |
| 444 self.assertEqual(expected, actual) | |
| 445 files = [ | |
| 446 u'tests/trace_inputs/child1.py'.replace('/', os.path.sep), | |
| 447 u'tests/trace_inputs/child2.py'.replace('/', os.path.sep), | |
| 448 u'tests/trace_inputs/files1/'.replace('/', os.path.sep), | |
| 449 u'tests/trace_inputs/test_file.txt'.replace('/', os.path.sep), | |
| 450 u'tests/trace_inputs_smoke_test.py'.replace('/', os.path.sep), | |
| 451 u'trace_inputs.py', | |
| 452 ] | |
| 453 def blacklist(f): | |
| 454 return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn')) | |
| 455 simplified = trace_inputs.extract_directories( | |
| 456 file_path.get_native_path_case(unicode(ROOT_DIR)), | |
| 457 results.files, | |
| 458 blacklist) | |
| 459 self.assertEqual(files, [f.path for f in simplified]) | |
| 460 | |
| 461 def test_trace_multiple(self): | |
| 462 # Starts parallel threads and trace parallel child processes simultaneously. | |
| 463 # Some are started from 'tests' directory, others from this script's | |
| 464 # directory. One trace fails. Verify everything still goes one. | |
| 465 parallel = 8 | |
| 466 | |
| 467 def trace(tracer, cmd, cwd, tracename): | |
| 468 resultcode, output = tracer.trace(cmd, cwd, tracename, True) | |
| 469 return (tracename, resultcode, output) | |
| 470 | |
| 471 with threading_utils.ThreadPool(parallel, parallel, 0) as pool: | |
| 472 api = trace_inputs.get_api() | |
| 473 with api.get_tracer(self.log) as tracer: | |
| 474 pool.add_task( | |
| 475 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace1') | |
| 476 pool.add_task( | |
| 477 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace2') | |
| 478 pool.add_task( | |
| 479 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace3') | |
| 480 pool.add_task( | |
| 481 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace4') | |
| 482 # Have this one fail since it's started from the wrong directory. | |
| 483 pool.add_task( | |
| 484 0, trace, tracer, self.get_child_command(False), self.cwd, 'trace5') | |
| 485 pool.add_task( | |
| 486 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace6') | |
| 487 pool.add_task( | |
| 488 0, trace, tracer, self.get_child_command(False), ROOT_DIR, 'trace7') | |
| 489 pool.add_task( | |
| 490 0, trace, tracer, self.get_child_command(True), self.cwd, 'trace8') | |
| 491 trace_results = pool.join() | |
| 492 def blacklist(f): | |
| 493 return f.endswith(('.pyc', 'do_not_care.txt', '.git', '.svn')) | |
| 494 actual_results = api.parse_log(self.log, blacklist, None) | |
| 495 self.assertEqual(8, len(trace_results)) | |
| 496 self.assertEqual(8, len(actual_results)) | |
| 497 | |
| 498 # Convert to dict keyed on the trace name, simpler to verify. | |
| 499 trace_results = dict((i[0], i[1:]) for i in trace_results) | |
| 500 actual_results = dict((x.pop('trace'), x) for x in actual_results) | |
| 501 self.assertEqual(sorted(trace_results), sorted(actual_results)) | |
| 502 | |
| 503 # It'd be nice to start different kinds of processes. | |
| 504 expected_results = [ | |
| 505 self._gen_dict_full(), | |
| 506 self._gen_dict_full_gyp(), | |
| 507 self._gen_dict_full(), | |
| 508 self._gen_dict_full_gyp(), | |
| 509 self._gen_dict_wrong_path(), | |
| 510 self._gen_dict_full_gyp(), | |
| 511 self._gen_dict_full(), | |
| 512 self._gen_dict_full_gyp(), | |
| 513 ] | |
| 514 self.assertEqual(len(expected_results), len(trace_results)) | |
| 515 | |
| 516 # See the comment above about the trace that fails because it's started from | |
| 517 # the wrong directory. | |
| 518 busted = 4 | |
| 519 for index, key in enumerate(sorted(actual_results)): | |
| 520 self.assertEqual('trace%d' % (index + 1), key) | |
| 521 self.assertEqual(2, len(trace_results[key])) | |
| 522 # returncode | |
| 523 self.assertEqual(0 if index != busted else 2, trace_results[key][0]) | |
| 524 # output | |
| 525 self.assertEqual(actual_results[key]['output'], trace_results[key][1]) | |
| 526 | |
| 527 self.assertEqual(['output', 'results'], sorted(actual_results[key])) | |
| 528 results = actual_results[key]['results'] | |
| 529 results = results.strip_root(unicode(ROOT_DIR)) | |
| 530 actual = results.flatten() | |
| 531 self.assertTrue(actual['root'].pop('pid')) | |
| 532 if index != busted: | |
| 533 self.assertTrue(actual['root']['children'][0].pop('pid')) | |
| 534 self.assertEqual(expected_results[index], actual) | |
| 535 | |
| 536 if sys.platform != 'win32': | |
| 537 def test_trace_symlink(self): | |
| 538 expected = { | |
| 539 'root': { | |
| 540 'children': [], | |
| 541 'command': [ | |
| 542 self.executable, | |
| 543 os.path.join('trace_inputs', 'symlink.py'), | |
| 544 ], | |
| 545 'executable': self.real_executable, | |
| 546 'files': [ | |
| 547 { | |
| 548 'mode': MODE_R, | |
| 549 'path': os.path.join(REL_DATA, 'files2', 'bar'), | |
| 550 'size': self._size(REL_DATA, 'files2', 'bar'), | |
| 551 }, | |
| 552 { | |
| 553 'mode': MODE_R, | |
| 554 'path': os.path.join(REL_DATA, 'files2', 'foo'), | |
| 555 'size': self._size(REL_DATA, 'files2', 'foo'), | |
| 556 }, | |
| 557 { | |
| 558 'mode': MODE_R, | |
| 559 'path': os.path.join(REL_DATA, 'symlink.py'), | |
| 560 'size': self._size(REL_DATA, 'symlink.py'), | |
| 561 }, | |
| 562 ], | |
| 563 'initial_cwd': self.initial_cwd, | |
| 564 }, | |
| 565 } | |
| 566 cmd = [sys.executable, os.path.join('trace_inputs', 'symlink.py')] | |
| 567 results = self._execute_trace(cmd) | |
| 568 actual = results.flatten() | |
| 569 self.assertTrue(actual['root'].pop('pid')) | |
| 570 self.assertEqual(expected, actual) | |
| 571 files = [ | |
| 572 # In particular, the symlink is *not* resolved. | |
| 573 u'tests/trace_inputs/files2/'.replace('/', os.path.sep), | |
| 574 u'tests/trace_inputs/symlink.py'.replace('/', os.path.sep), | |
| 575 ] | |
| 576 def blacklist(f): | |
| 577 return f.endswith(('.pyc', '.svn', 'do_not_care.txt')) | |
| 578 simplified = trace_inputs.extract_directories( | |
| 579 unicode(ROOT_DIR), results.files, blacklist) | |
| 580 self.assertEqual(files, [f.path for f in simplified]) | |
| 581 | |
| 582 def test_trace_quoted(self): | |
| 583 results = self._execute_trace([sys.executable, '-c', 'print("hi")']) | |
| 584 expected = { | |
| 585 'root': { | |
| 586 'children': [], | |
| 587 'command': [ | |
| 588 self.executable, | |
| 589 '-c', | |
| 590 'print("hi")', | |
| 591 ], | |
| 592 'executable': self.real_executable, | |
| 593 'files': [], | |
| 594 'initial_cwd': self.initial_cwd, | |
| 595 }, | |
| 596 } | |
| 597 actual = results.flatten() | |
| 598 self.assertTrue(actual['root'].pop('pid')) | |
| 599 self.assertEqual(expected, actual) | |
| 600 | |
| 601 def _touch_expected(self, command): | |
| 602 # Looks for file that were touched but not opened, using different apis. | |
| 603 results = self._execute_trace( | |
| 604 [sys.executable, os.path.join('trace_inputs', 'touch_only.py'), command]) | |
| 605 expected = { | |
| 606 'root': { | |
| 607 'children': [], | |
| 608 'command': [ | |
| 609 self.executable, | |
| 610 os.path.join('trace_inputs', 'touch_only.py'), | |
| 611 command, | |
| 612 ], | |
| 613 'executable': self.real_executable, | |
| 614 'files': [ | |
| 615 { | |
| 616 'mode': MODE_T, | |
| 617 'path': os.path.join(REL_DATA, 'test_file.txt'), | |
| 618 'size': self._size(REL_DATA, 'test_file.txt'), | |
| 619 }, | |
| 620 { | |
| 621 'mode': MODE_R, | |
| 622 'path': os.path.join(REL_DATA, 'touch_only.py'), | |
| 623 'size': self._size(REL_DATA, 'touch_only.py'), | |
| 624 }, | |
| 625 ], | |
| 626 'initial_cwd': self.initial_cwd, | |
| 627 }, | |
| 628 } | |
| 629 if sys.platform != 'linux2': | |
| 630 # TODO(maruel): Remove once properly implemented. | |
| 631 expected['root']['files'].pop(0) | |
| 632 | |
| 633 actual = results.flatten() | |
| 634 self.assertTrue(actual['root'].pop('pid')) | |
| 635 self.assertEqual(expected, actual) | |
| 636 | |
| 637 def test_trace_touch_only_access(self): | |
| 638 self._touch_expected('access') | |
| 639 | |
| 640 def test_trace_touch_only_isfile(self): | |
| 641 self._touch_expected('isfile') | |
| 642 | |
| 643 def test_trace_touch_only_stat(self): | |
| 644 self._touch_expected('stat') | |
| 645 | |
| 646 def test_trace_tricky_filename(self): | |
| 647 # TODO(maruel): On Windows, it's using the current code page so some | |
| 648 # characters can't be represented. As a nice North American, hard code the | |
| 649 # string to something representable in code page 1252. The exact code page | |
| 650 # depends on the user system. | |
| 651 if sys.platform == 'win32': | |
| 652 filename = u'foo, bar, ~p#o,,ué^t%t .txt' | |
| 653 else: | |
| 654 filename = u'foo, bar, ~p#o,,ué^t%t 和平.txt' | |
| 655 | |
| 656 exe = os.path.join(self.tempdir, 'tricky_filename.py') | |
| 657 shutil.copyfile( | |
| 658 os.path.join(self.cwd, 'trace_inputs', 'tricky_filename.py'), exe) | |
| 659 expected = { | |
| 660 'root': { | |
| 661 'children': [], | |
| 662 'command': [ | |
| 663 self.executable, | |
| 664 exe, | |
| 665 ], | |
| 666 'executable': self.real_executable, | |
| 667 'files': [ | |
| 668 { | |
| 669 'mode': MODE_W, | |
| 670 'path': filename, | |
| 671 'size': long(len('Bingo!')), | |
| 672 }, | |
| 673 { | |
| 674 'mode': MODE_R, | |
| 675 'path': u'tricky_filename.py', | |
| 676 'size': self._size(REL_DATA, 'tricky_filename.py'), | |
| 677 }, | |
| 678 ], | |
| 679 'initial_cwd': self.tempdir if sys.platform != 'win32' else None, | |
| 680 }, | |
| 681 } | |
| 682 | |
| 683 api = trace_inputs.get_api() | |
| 684 returncode, output = trace_inputs.trace( | |
| 685 self.log, [exe], self.tempdir, api, True) | |
| 686 self.assertEqual('', output) | |
| 687 self.assertEqual(0, returncode) | |
| 688 data = api.parse_log(self.log, lambda _: False, None) | |
| 689 self.assertEqual(1, len(data)) | |
| 690 if 'exception' in data[0]: | |
| 691 raise data[0]['exception'][0], \ | |
| 692 data[0]['exception'][1], \ | |
| 693 data[0]['exception'][2] | |
| 694 actual = data[0]['results'].strip_root(self.tempdir).flatten() | |
| 695 self.assertTrue(actual['root'].pop('pid')) | |
| 696 self.assertEqual(expected, actual) | |
| 697 trace_inputs.get_api().clean_trace(self.log) | |
| 698 files = sorted( | |
| 699 unicodedata.normalize('NFC', i) | |
| 700 for i in os.listdir(unicode(self.tempdir))) | |
| 701 self.assertEqual([filename, 'tricky_filename.py'], files) | |
| 702 | |
| 703 | |
| 704 if __name__ == '__main__': | |
| 705 VERBOSE = '-v' in sys.argv | |
| 706 logging.basicConfig(level=logging.DEBUG if VERBOSE else logging.ERROR) | |
| 707 if VERBOSE: | |
| 708 unittest.TestCase.maxDiff = None | |
| 709 # Necessary for the dtrace logger to work around execve() hook. See | |
| 710 # trace_inputs.py for more details. | |
| 711 os.environ['TRACE_INPUTS_DTRACE_ENABLE_EXECVE'] = '1' | |
| 712 unittest.main() | |
| OLD | NEW |