1#!/usr/bin/python 2# Copyright (c) 2011 The Native Client Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Testing Library For Nacl. 7 8""" 9 10from __future__ import print_function 11 12import atexit 13import difflib 14import os 15import re 16import shutil 17import signal 18import subprocess 19import sys 20import tempfile 21import threading 22 23sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 24import pynacl.platform 25 26 27# Windows does not fully implement os.times functionality. If 28# _GetTimesPosix were used, the fields for CPU time used in user and 29# in kernel mode by the process will both be zero. Instead, we must 30# use the ctypes module and access windll's kernel32 interface to 31# extract the CPU usage information. 32 33if sys.platform[:3] == 'win': 34 import ctypes 35 36 37class SubprocessCpuTimer: 38 """Timer used to measure user and kernel CPU time expended by a subprocess. 39 40 A new object of this class should be instantiated just before the 41 subprocess is created, and after the subprocess is finished and 42 waited for (via the wait method of the Popen object), the elapsed 43 time can be obtained by invoking the ElapsedCpuTime of the 44 SubprocessCpuTimer instance. 45 46 """ 47 48 WIN32_PROCESS_TIMES_TICKS_PER_SECOND = 1.0e7 49 50 # use a class variable to avoid slicing at run-time 51 _use_proc_handle = sys.platform[:3] == 'win' 52 53 54 @staticmethod 55 def _GetTimePosix(): 56 try: 57 t = os.times() 58 except OSError: 59 # This works around a bug in the calling conventions for the 60 # times() system call on Linux. This syscall returns a number 61 # of clock ticks since an arbitrary time in the past, but if 62 # this happens to be between -4095 and -1, it is interpreted as 63 # an errno value, and we get an exception here. 64 # Returning 0 as a dummy value may result in ElapsedCpuTime() 65 # below returning a negative value. This is OK for our use 66 # because a test that takes too long is likely to be caught 67 # elsewhere. 68 if sys.platform == "linux2": 69 return 0 70 raise 71 else: 72 return t[2] + t[3] 73 74 75 @staticmethod 76 def _GetTimeWindows(proc_handle): 77 if proc_handle is None: 78 return 0 79 creation_time = ctypes.c_ulonglong() 80 exit_time = ctypes.c_ulonglong() 81 kernel_time = ctypes.c_ulonglong() 82 user_time = ctypes.c_ulonglong() 83 rc = ctypes.windll.kernel32.GetProcessTimes( 84 int(proc_handle._handle), 85 ctypes.byref(creation_time), 86 ctypes.byref(exit_time), 87 ctypes.byref(kernel_time), 88 ctypes.byref(user_time)) 89 if not rc: 90 print('Could not obtain process time', file=sys.stderr) 91 return 0 92 return ((kernel_time.value + user_time.value) 93 / SubprocessCpuTimer.WIN32_PROCESS_TIMES_TICKS_PER_SECOND) 94 95 96 @staticmethod 97 def _GetTime(proc_handle): 98 if SubprocessCpuTimer._use_proc_handle: 99 return SubprocessCpuTimer._GetTimeWindows(proc_handle) 100 return SubprocessCpuTimer._GetTimePosix() 101 102 103 def __init__(self): 104 self._start_time = self._GetTime(None) 105 106 107 def ElapsedCpuTime(self, proc_handle): 108 return self._GetTime(proc_handle) - self._start_time 109 110def PopenBufSize(): 111 return 1000 * 1000 112 113 114def CommunicateWithTimeout(proc, input_data=None, timeout=None): 115 if timeout == 0: 116 timeout = None 117 118 result = [] 119 def Target(): 120 result.append(list(proc.communicate(input_data))) 121 122 thread = threading.Thread(target=Target) 123 thread.start() 124 thread.join(timeout) 125 if thread.is_alive(): 126 sys.stderr.write('\nAttempting to kill test due to timeout!\n') 127 # This will kill the process which should force communicate to return with 128 # any partial output. 129 pynacl.platform.KillSubprocessAndChildren(proc) 130 # Thus result should ALWAYS contain something after this join. 131 thread.join() 132 sys.stderr.write('\n\nKilled test due to timeout!\n') 133 # Also append to stderr. 134 result[0][1] += '\n\nKilled test due to timeout!\n' 135 returncode = -9 136 else: 137 returncode = proc.returncode 138 assert len(result) == 1 139 return tuple(result[0]) + (returncode,) 140 141 142def RunTestWithInput(cmd, input_data, timeout=None): 143 """Run a test where we only care about the return code.""" 144 assert type(cmd) == list 145 failed = 0 146 timer = SubprocessCpuTimer() 147 p = None 148 try: 149 sys.stdout.flush() # Make sure stdout stays in sync on the bots. 150 if type(input_data) == str: 151 p = subprocess.Popen(cmd, 152 bufsize=PopenBufSize(), 153 stdin=subprocess.PIPE) 154 _, _, retcode = CommunicateWithTimeout(p, input_data, timeout=timeout) 155 else: 156 p = subprocess.Popen(cmd, 157 bufsize=PopenBufSize(), 158 stdin=input_data) 159 _, _, retcode = CommunicateWithTimeout(p, timeout=timeout) 160 except OSError: 161 print('exception: ' + str(sys.exc_info()[1])) 162 retcode = 0 163 failed = 1 164 165 if p is None: 166 return (0, 0, 1) 167 return (timer.ElapsedCpuTime(p), retcode, failed) 168 169 170def RunTestWithInputOutput(cmd, input_data, capture_stderr=True, timeout=None): 171 """Run a test where we also care about stdin/stdout/stderr. 172 173 NOTE: this function may have problems with arbitrarily 174 large input or output, especially on windows 175 NOTE: input_data can be either a string or or a file like object, 176 file like objects may be better for large input/output 177 """ 178 assert type(cmd) == list 179 stdout = '' 180 stderr = '' 181 failed = 0 182 183 p = None 184 timer = SubprocessCpuTimer() 185 try: 186 # Python on windows does not include any notion of SIGPIPE. On 187 # Linux and OSX, Python installs a signal handler for SIGPIPE that 188 # sets the handler to SIG_IGN so that syscalls return -1 with 189 # errno equal to EPIPE, and translates those to exceptions; 190 # unfortunately, the subprocess module fails to reset the handler 191 # for SIGPIPE to SIG_DFL, and the SIG_IGN behavior is inherited. 192 # subprocess.Popen's preexec_fn is apparently okay to use on 193 # Windows, as long as its value is None. 194 195 if hasattr(signal, 'SIGPIPE'): 196 no_pipe = lambda : signal.signal(signal.SIGPIPE, signal.SIG_DFL) 197 else: 198 no_pipe = None 199 200 # Only capture stderr if capture_stderr is true 201 p_stderr = subprocess.PIPE if capture_stderr else None 202 203 if type(input_data) == str: 204 p = subprocess.Popen(cmd, 205 bufsize=PopenBufSize(), 206 stdin=subprocess.PIPE, 207 stderr=p_stderr, 208 stdout=subprocess.PIPE, 209 preexec_fn = no_pipe) 210 stdout, stderr, retcode = CommunicateWithTimeout( 211 p, input_data, timeout=timeout) 212 else: 213 # input_data is a file like object 214 p = subprocess.Popen(cmd, 215 bufsize=PopenBufSize(), 216 stdin=input_data, 217 stderr=p_stderr, 218 stdout=subprocess.PIPE, 219 preexec_fn = no_pipe) 220 stdout, stderr, retcode = CommunicateWithTimeout(p, timeout=timeout) 221 except OSError, x: 222 if x.errno == 10: 223 print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@') 224 print('ignoring exception', str(sys.exc_info()[1])) 225 print('return code NOT checked') 226 print('this seems to be a windows issue') 227 print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@') 228 failed = 0 229 retcode = 0 230 else: 231 print('exception: ' + str(sys.exc_info()[1])) 232 retcode = 0 233 failed = 1 234 if p is None: 235 cpu_time_consumed = 0 236 else: 237 cpu_time_consumed = timer.ElapsedCpuTime(p) 238 return (cpu_time_consumed, retcode, failed, stdout, stderr) 239 240 241def DiffStringsIgnoringWhiteSpace(a, b): 242 a = a.splitlines() 243 b = b.splitlines() 244 # NOTE: the whitespace stuff seems to be broken in python 245 cruncher = difflib.SequenceMatcher(lambda x: x in ' \t\r', a, b) 246 247 for group in cruncher.get_grouped_opcodes(): 248 eq = True 249 for tag, i1, i2, j1, j2 in group: 250 if tag != 'equal': 251 eq = False 252 break 253 if eq: continue 254 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] 255 yield '@@ -%d,%d +%d,%d @@\n' % (i1+1, i2-i1, j1+1, j2-j1) 256 257 for tag, i1, i2, j1, j2 in group: 258 if tag == 'equal': 259 for line in a[i1:i2]: 260 yield ' [' + line + ']' 261 continue 262 if tag == 'replace' or tag == 'delete': 263 for line in a[i1:i2]: 264 yield '-[' + repr(line) + ']' 265 if tag == 'replace' or tag == 'insert': 266 for line in b[j1:j2]: 267 yield '+[' + repr(line) + ']' 268 269 270def RegexpFilterLines(regexp, inverse, group_only, lines): 271 """Apply regexp to filter lines of text, keeping only those lines 272 that match. 273 274 Any carriage return / newline sequence is turned into a newline. 275 276 Args: 277 regexp: A regular expression, only lines that match are kept 278 inverse: Only keep lines that do not match 279 group_only: replace matching lines with the regexp groups, 280 text outside the groups are omitted, useful for 281 eliminating file names that might change, etc). 282 283 lines: A string containing newline-separated lines of text 284 285 Return: 286 Filtered lines of text, newline separated. 287 """ 288 289 result = [] 290 nfa = re.compile(regexp) 291 for line in lines.split('\n'): 292 if line.endswith('\r'): 293 line = line[:-1] 294 mobj = nfa.search(line) 295 if mobj and inverse: 296 continue 297 if not mobj and not inverse: 298 continue 299 300 if group_only: 301 matched_strings = [] 302 for s in mobj.groups(): 303 if s is not None: 304 matched_strings.append(s) 305 result.append(''.join(matched_strings)) 306 else: 307 result.append(line) 308 309 return '\n'.join(result) 310 311 312def MakeTempDir(env, **kwargs): 313 """Create a temporary directory and arrange to clean it up on exit. 314 315 Passes arguments through to tempfile.mkdtemp 316 """ 317 temporary_dir = tempfile.mkdtemp(**kwargs) 318 def Cleanup(): 319 try: 320 # Try to remove the dir but only if it exists. Some tests may clean up 321 # after themselves. 322 if os.path.exists(temporary_dir): 323 shutil.rmtree(temporary_dir) 324 except BaseException as e: 325 sys.stderr.write('Unable to delete dir %s on exit: %s\n' % ( 326 temporary_dir, e)) 327 atexit.register(Cleanup) 328 return temporary_dir 329 330def MakeTempFile(env, **kwargs): 331 """Create a temporary file and arrange to clean it up on exit. 332 333 Passes arguments through to tempfile.mkstemp 334 """ 335 handle, path = tempfile.mkstemp(**kwargs) 336 def Cleanup(): 337 try: 338 # Try to remove the file but only if it exists. Some tests may clean up 339 # after themselves. 340 if os.path.exists(path): 341 os.unlink(path) 342 except BaseException as e: 343 sys.stderr.write('Unable to delete file %s on exit: %s\n' % ( 344 path, e)) 345 atexit.register(Cleanup) 346 return handle, path 347