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