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 |