1#
2#  testcase.py:  Control of test case execution.
3#
4#  Subversion is a tool for revision control.
5#  See http://subversion.tigris.org for more information.
6#
7# ====================================================================
8#    Licensed to the Apache Software Foundation (ASF) under one
9#    or more contributor license agreements.  See the NOTICE file
10#    distributed with this work for additional information
11#    regarding copyright ownership.  The ASF licenses this file
12#    to you under the Apache License, Version 2.0 (the
13#    "License"); you may not use this file except in compliance
14#    with the License.  You may obtain a copy of the License at
15#
16#      http://www.apache.org/licenses/LICENSE-2.0
17#
18#    Unless required by applicable law or agreed to in writing,
19#    software distributed under the License is distributed on an
20#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21#    KIND, either express or implied.  See the License for the
22#    specific language governing permissions and limitations
23#    under the License.
24######################################################################
25
26import os, types, sys
27
28import svntest
29
30# if somebody does a "from testcase import *", they only get these names
31__all__ = ['_XFail', '_Wimp', '_Skip', '_SkipUnless',
32           '_SkipDumpLoadCrossCheck']
33
34RESULT_OK = 'ok'
35RESULT_FAIL = 'fail'
36RESULT_SKIP = 'skip'
37
38
39class TextColors:
40  '''Some ANSI terminal constants for output color'''
41  ENDC = '\033[0;m'
42  FAILURE = '\033[1;31m'
43  SUCCESS = '\033[1;32m'
44
45  @classmethod
46  def disable(cls):
47    cls.ENDC = ''
48    cls.FAILURE = ''
49    cls.SUCCESS = ''
50
51  @classmethod
52  def success(cls, str):
53    return lambda: cls.SUCCESS + str + cls.ENDC
54
55  @classmethod
56  def failure(cls, str):
57    return lambda: cls.FAILURE + str + cls.ENDC
58
59
60if not sys.stdout.isatty() or sys.platform == 'win32':
61  TextColors.disable()
62
63
64class TestCase:
65  """A thing that can be tested.  This is an abstract class with
66  several methods that need to be overridden."""
67
68  _result_map = {
69    RESULT_OK:   (0, TextColors.success('PASS: '), True),
70    RESULT_FAIL: (1, TextColors.failure('FAIL: '), False),
71    RESULT_SKIP: (2, TextColors.success('SKIP: '), True),
72    }
73
74  def __init__(self, delegate=None, cond_func=lambda: True, doc=None, wip=None,
75               issues=None):
76    """Create a test case instance based on DELEGATE.
77
78    COND_FUNC is a callable that is evaluated at test run time and should
79    return a boolean value that determines how a pass or failure is
80    interpreted: see the specialized kinds of test case such as XFail and
81    Skip for details.  The evaluation of COND_FUNC is deferred so that it
82    can base its decision on useful bits of information that are not
83    available at __init__ time (like the fact that we're running over a
84    particular RA layer).
85
86    DOC is ...
87
88    WIP is a string describing the reason for the work-in-progress
89    """
90    assert hasattr(cond_func, '__call__')
91
92    self._delegate = delegate
93    self._cond_func = cond_func
94    self.description = doc or delegate.description
95    self.inprogress = wip
96    self.issues = issues
97
98  def get_function_name(self):
99    """Return the name of the python function implementing the test."""
100    return self._delegate.get_function_name()
101
102  def get_sandbox_name(self):
103    """Return the name that should be used for the sandbox.
104
105    If a sandbox should not be constructed, this method returns None.
106    """
107    return self._delegate.get_sandbox_name()
108
109  def set_issues(self, issues):
110    """Set the issues associated with this test."""
111    self.issues = issues
112
113  def run(self, sandbox):
114    """Run the test within the given sandbox."""
115    return self._delegate.run(sandbox)
116
117  def list_mode(self):
118    return ''
119
120  def results(self, result):
121    # if our condition applied, then use our result map. otherwise, delegate.
122    if self._cond_func():
123      val = list(self._result_map[result])
124      val[1] = val[1]()
125      return val
126    return self._delegate.results(result)
127
128
129class FunctionTestCase(TestCase):
130  """A TestCase based on a naked Python function object.
131
132  FUNC should be a function that returns None on success and throws an
133  svntest.Failure exception on failure.  It should have a brief
134  docstring describing what it does (and fulfilling certain conditions).
135  FUNC must take one argument, an Sandbox instance.  (The sandbox name
136  is derived from the file name in which FUNC was defined)
137  """
138
139  def __init__(self, func, issues=None, skip_cross_check=False):
140    # it better be a function that accepts an sbox parameter and has a
141    # docstring on it.
142    assert isinstance(func, types.FunctionType)
143
144    name = func.__name__
145
146    assert func.__code__.co_argcount == 1, \
147        '%s must take an sbox argument' % name
148
149    doc = func.__doc__.strip()
150    assert doc, '%s must have a docstring' % name
151
152    # enforce stylistic guidelines for the function docstrings:
153    # - no longer than 50 characters
154    # - should not end in a period
155    # - should not be capitalized
156    assert len(doc) <= 50, \
157        "%s's docstring must be 50 characters or less" % name
158    assert doc[-1] != '.', \
159        "%s's docstring should not end in a period" % name
160    assert doc[0].lower() == doc[0], \
161        "%s's docstring should not be capitalized" % name
162
163    TestCase.__init__(self, doc=doc, issues=issues)
164    self.func = func
165    self.skip_cross_check = skip_cross_check
166
167  def get_function_name(self):
168    return self.func.__name__
169
170  def get_sandbox_name(self):
171    """Base the sandbox's name on the name of the file in which the
172    function was defined."""
173
174    filename = self.func.__code__.co_filename
175    return os.path.splitext(os.path.basename(filename))[0]
176
177  def run(self, sandbox):
178    result = self.func(sandbox)
179    sandbox.verify(skip_cross_check = self.skip_cross_check)
180    return result
181
182
183class _XFail(TestCase):
184  """A test that is expected to fail, if its condition is true."""
185
186  _result_map = {
187    RESULT_OK:   (1, TextColors.failure('XPASS:'), False),
188    RESULT_FAIL: (0, TextColors.success('XFAIL:'), True),
189    RESULT_SKIP: (2, TextColors.success('SKIP: '), True),
190    }
191
192  def __init__(self, test_case, cond_func=lambda: True, wip=None,
193               issues=None):
194    """Create an XFail instance based on TEST_CASE.  COND_FUNC is a
195    callable that is evaluated at test run time and should return a
196    boolean value.  If COND_FUNC returns true, then TEST_CASE is
197    expected to fail (and a pass is considered an error); otherwise,
198    TEST_CASE is run normally.  The evaluation of COND_FUNC is
199    deferred so that it can base its decision on useful bits of
200    information that are not available at __init__ time (like the fact
201    that we're running over a particular RA layer).
202
203    WIP is ...
204
205    ISSUES is an issue number (or a list of issue numbers) tracking this."""
206
207    TestCase.__init__(self, create_test_case(test_case), cond_func, wip=wip,
208                      issues=issues)
209
210  def list_mode(self):
211    # basically, the only possible delegate is a Skip test. favor that mode.
212    return self._delegate.list_mode() or 'XFAIL'
213
214
215class _Wimp(_XFail):
216  """Like XFail, but indicates a work-in-progress: an unexpected pass
217  is not considered a test failure."""
218
219  _result_map = {
220    RESULT_OK:   (0, TextColors.success('XPASS:'), True),
221    RESULT_FAIL: (0, TextColors.success('XFAIL:'), True),
222    RESULT_SKIP: (2, TextColors.success('SKIP: '), True),
223    }
224
225  def __init__(self, wip, test_case, cond_func=lambda: True, issues=None):
226    _XFail.__init__(self, test_case, cond_func, wip, issues)
227
228
229class _Skip(TestCase):
230  """A test that will be skipped if its conditional is true."""
231
232  def __init__(self, test_case, cond_func=lambda: True, issues=None):
233    """Create a Skip instance based on TEST_CASE.  COND_FUNC is a
234    callable that is evaluated at test run time and should return a
235    boolean value.  If COND_FUNC returns true, then TEST_CASE is
236    skipped; otherwise, TEST_CASE is run normally.
237    The evaluation of COND_FUNC is deferred so that it can base its
238    decision on useful bits of information that are not available at
239    __init__ time (like the fact that we're running over a
240    particular RA layer)."""
241
242    TestCase.__init__(self, create_test_case(test_case), cond_func,
243                      issues=issues)
244
245  def list_mode(self):
246    if self._cond_func():
247      return 'SKIP'
248    return self._delegate.list_mode()
249
250  def get_sandbox_name(self):
251    if self._cond_func():
252      return None
253    return self._delegate.get_sandbox_name()
254
255  def run(self, sandbox):
256    if self._cond_func():
257      raise svntest.Skip
258    return self._delegate.run(sandbox)
259
260
261class _SkipUnless(_Skip):
262  """A test that will be skipped if its conditional is false."""
263
264  def __init__(self, test_case, cond_func):
265    _Skip.__init__(self, test_case, lambda c=cond_func: not c())
266
267
268class _SkipDumpLoadCrossCheck(TestCase):
269  """A test that will skip the post-test dump/load cross-check."""
270
271  def __init__(self, test_case, cond_func=lambda: True, wip=None,
272               issues=None):
273    TestCase.__init__(self,
274                      create_test_case(test_case, skip_cross_check=True),
275                      cond_func, wip=wip, issues=issues)
276
277
278def create_test_case(func, issues=None, skip_cross_check=False):
279  if isinstance(func, TestCase):
280    return func
281  else:
282    return FunctionTestCase(func, issues=issues,
283                            skip_cross_check=skip_cross_check)
284
285
286# Various decorators to make declaring tests as such simpler
287def XFail_deco(cond_func = lambda: True):
288  def _second(func):
289    if isinstance(func, TestCase):
290      return _XFail(func, cond_func, issues=func.issues)
291    else:
292      return _XFail(func, cond_func)
293
294  return _second
295
296
297def Wimp_deco(wip, cond_func = lambda: True):
298  def _second(func):
299    if isinstance(func, TestCase):
300      return _Wimp(wip, func, cond_func, issues=func.issues)
301    else:
302      return _Wimp(wip, func, cond_func)
303
304  return _second
305
306
307def Skip_deco(cond_func = lambda: True):
308  def _second(func):
309    if isinstance(func, TestCase):
310      return _Skip(func, cond_func, issues=func.issues)
311    else:
312      return _Skip(func, cond_func)
313
314  return _second
315
316
317def SkipUnless_deco(cond_func):
318  def _second(func):
319    if isinstance(func, TestCase):
320      return _Skip(func, lambda c=cond_func: not c(), issues=func.issues)
321    else:
322      return _Skip(func, lambda c=cond_func: not c())
323
324  return _second
325
326
327def Issues_deco(*issues):
328  def _second(func):
329    if isinstance(func, TestCase):
330      # if the wrapped thing is already a test case, just set the issues
331      func.set_issues(issues)
332      return func
333
334    else:
335      # we need to wrap the function
336      return create_test_case(func, issues=issues)
337
338  return _second
339
340def SkipDumpLoadCrossCheck_deco(cond_func = lambda: True):
341  def _second(func):
342    if isinstance(func, TestCase):
343      return _SkipDumpLoadCrossCheck(func, cond_func, issues=func.issues)
344    else:
345      return _SkipDumpLoadCrossCheck(func, cond_func)
346
347  return _second
348
349
350# Create a singular alias, for linguistic correctness
351Issue_deco = Issues_deco
352