summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-30 21:13:39 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-30 21:13:39 -0400
commitfd538f1428d6e20ccbd0c71520a50ee7b1a311b4 (patch)
tree12c44fec5b00946a748d667a5417a3e74db402b6
parent4df62c3081c19709b8303565d0bd0ac39aa7f91a (diff)
downloadpasslib-fd538f1428d6e20ccbd0c71520a50ee7b1a311b4.tar.gz
unittest cleanups, better coverage, etc
* split ut2 backports into separate module to keep them distinct from customizations * added backport of skip() / skipIf(), simplified a bunch of code * "PASSLIB_TESTS" env var renamed to "PASSLIB_TEST_MODE", has one of three values (quick,default,full) * assertWarningList() can now be used as context manager * added TestCase.mktemp(), and some capability tests via TestCase.require_xxx() * HandlerCase - subclasses can now modify do_xxx() settings and context using unified interface. - defaults to lower number of rounds for all hashes, to speed up UTs - create_backend_case() is now classmethod that yields multiple backends - added test to ensure os_crypt hashes forbid NULL chars - EncodingHandlerMixin for common tests of 'encoding' keyword
-rw-r--r--passlib/tests/backports.py329
-rw-r--r--passlib/tests/tox_support.py34
-rw-r--r--passlib/tests/utils.py957
-rw-r--r--passlib/utils/compat.py15
-rw-r--r--tox.ini87
5 files changed, 823 insertions, 599 deletions
diff --git a/passlib/tests/backports.py b/passlib/tests/backports.py
new file mode 100644
index 0000000..bde41cb
--- /dev/null
+++ b/passlib/tests/backports.py
@@ -0,0 +1,329 @@
+"""backports of needed unittest2 features"""
+#=========================================================
+#imports
+#=========================================================
+from __future__ import with_statement
+#core
+import logging; log = logging.getLogger(__name__)
+import re
+import sys
+##from warnings import warn
+#site
+#pkg
+from passlib.utils.compat import base_string_types
+#local
+__all__ = [
+ "TestCase",
+ "skip", "skipIf", "skipUnless"
+ "catch_warnings",
+]
+
+#=========================================================
+# import latest unittest module available
+#=========================================================
+try:
+ import unittest2 as unittest
+ ut_version = 2
+except ImportError:
+ import unittest
+ if sys.version_info < (2,7) or (3,0) <= sys.version_info < (3,2):
+ # older versions of python will need to install the unittest2
+ # backport (named unittest2_3k for 3.0/3.1)
+ ##warn("please install unittest2 for python %d.%d, it will be required "
+ ## "as of passlib 1.x" % sys.version_info[:2])
+ ut_version = 1
+ else:
+ ut_version = 2
+
+#=========================================================
+# backport SkipTest support using nose
+#=========================================================
+if ut_version < 2:
+ # used to provide replacement SkipTest() error
+ from nose.plugins.skip import SkipTest
+
+ # hack up something to simulate skip() decorator
+ import functools
+ def skip(reason):
+ def decorator(test_item):
+ if isinstance(test_item, type) and issubclass(test_item, unittest.TestCase):
+ class skip_wrapper(test_item):
+ def setUp(self):
+ raise SkipTest(reason)
+ else:
+ @functools.wraps(test_item)
+ def skip_wrapper(*args, **kwargs):
+ raise SkipTest(reason)
+ return skip_wrapper
+ return decorator
+
+ def skipIf(condition, reason):
+ if condition:
+ return skip(reason)
+ else:
+ return lambda item: item
+
+ def skipUnless(condition, reason):
+ if condition:
+ return lambda item: item
+ else:
+ return skip(reason)
+
+else:
+ skip = unittest.skip
+ skipIf = unittest.skipIf
+ skipUnless = unittest.skipUnless
+
+#=========================================================
+# custom test harness
+#=========================================================
+class TestCase(unittest.TestCase):
+ """backports a number of unittest2 features in TestCase"""
+ #====================================================================
+ # backport some methods from unittest2
+ #====================================================================
+ if ut_version < 2:
+
+ #----------------------------------------------------------------
+ # simplistic backport of addCleanup() framework
+ #----------------------------------------------------------------
+ _cleanups = None
+
+ def addCleanup(self, function, *args, **kwds):
+ queue = self._cleanups
+ if queue is None:
+ queue = self._cleanups = []
+ queue.append((function, args, kwds))
+
+ def doCleanups(self):
+ queue = self._cleanups
+ while queue:
+ func, args, kwds = queue.pop()
+ func(*args, **kwds)
+
+ def tearDown(self):
+ self.doCleanups()
+ unittest.TestCase.tearDown(self)
+
+ #----------------------------------------------------------------
+ # backport skipTest (requires nose to work)
+ #----------------------------------------------------------------
+ def skipTest(self, reason):
+ raise SkipTest(reason)
+
+ #----------------------------------------------------------------
+ # backport various assert tests added in unittest2
+ #----------------------------------------------------------------
+ def assertIs(self, real, correct, msg=None):
+ if real is not correct:
+ std = "got %r, expected would be %r" % (real, correct)
+ msg = self._formatMessage(msg, std)
+ raise self.failureException(msg)
+
+ def assertIsNot(self, real, correct, msg=None):
+ if real is correct:
+ std = "got %r, expected would not be %r" % (real, correct)
+ msg = self._formatMessage(msg, std)
+ raise self.failureException(msg)
+
+ def assertIsInstance(self, obj, klass, msg=None):
+ if not isinstance(obj, klass):
+ std = "got %r, expected instance of %r" % (obj, klass)
+ msg = self._formatMessage(msg, std)
+ raise self.failureException(msg)
+
+ def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None):
+ """Fail if the two objects are unequal as determined by their
+ difference rounded to the given number of decimal places
+ (default 7) and comparing to zero, or by comparing that the
+ between the two objects is more than the given delta.
+
+ Note that decimal places (from zero) are usually not the same
+ as significant digits (measured from the most signficant digit).
+
+ If the two objects compare equal then they will automatically
+ compare almost equal.
+ """
+ if first == second:
+ # shortcut
+ return
+ if delta is not None and places is not None:
+ raise TypeError("specify delta or places not both")
+
+ if delta is not None:
+ if abs(first - second) <= delta:
+ return
+
+ standardMsg = '%s != %s within %s delta' % (repr(first),
+ repr(second),
+ repr(delta))
+ else:
+ if places is None:
+ places = 7
+
+ if round(abs(second-first), places) == 0:
+ return
+
+ standardMsg = '%s != %s within %r places' % (repr(first),
+ repr(second),
+ places)
+ msg = self._formatMessage(msg, standardMsg)
+ raise self.failureException(msg)
+
+ def assertLess(self, left, right, msg=None):
+ if left >= right:
+ std = "%r not less than %r" % (left, right)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ def assertGreater(self, left, right, msg=None):
+ if left <= right:
+ std = "%r not greater than %r" % (left, right)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ def assertGreaterEqual(self, left, right, msg=None):
+ if left < right:
+ std = "%r less than %r" % (left, right)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ def assertIn(self, elem, container, msg=None):
+ if elem not in container:
+ std = "%r not found in %r" % (elem, container)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ def assertNotIn(self, elem, container, msg=None):
+ if elem in container:
+ std = "%r unexpectedly in %r" % (elem, container)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ #----------------------------------------------------------------
+ # override some unittest1 methods to support _formatMessage
+ #----------------------------------------------------------------
+ def assertEqual(self, real, correct, msg=None):
+ if real != correct:
+ std = "got %r, expected would equal %r" % (real, correct)
+ msg = self._formatMessage(msg, std)
+ raise self.failureException(msg)
+
+ def assertNotEqual(self, real, correct, msg=None):
+ if real == correct:
+ std = "got %r, expected would not equal %r" % (real, correct)
+ msg = self._formatMessage(msg, std)
+ raise self.failureException(msg)
+
+ #----------------------------------------------------------------
+ # backport assertRegex() alias from 3.2 to 2.7/3.1
+ #----------------------------------------------------------------
+ if not hasattr(unittest.TestCase, "assertRegex"):
+ if hasattr(unittest.TestCase, "assertRegexpMatches"):
+ # was present in 2.7/3.1 under name assertRegexpMatches
+ assertRegex = unittest.TestCase.assertRegexpMatches
+ else:
+ # 3.0 and <= 2.6 didn't have this method at all
+ def assertRegex(self, text, expected_regex, msg=None):
+ """Fail the test unless the text matches the regular expression."""
+ if isinstance(expected_regex, base_string_types):
+ assert expected_regex, "expected_regex must not be empty."
+ expected_regex = re.compile(expected_regex)
+ if not expected_regex.search(text):
+ msg = msg or "Regex didn't match: "
+ std = '%r not found in %r' % (msg, expected_regex.pattern, text)
+ raise self.failureException(self._formatMessage(msg, std))
+
+ #============================================================
+ #eoc
+ #============================================================
+
+#=============================================================================
+# backport catch_warnings
+#=============================================================================
+try:
+ from warnings import catch_warnings
+except ImportError:
+ # catch_warnings wasn't added until py26.
+ # this adds backported copy from py26's stdlib
+ # so we can use it under py25.
+
+ class WarningMessage(object):
+
+ """Holds the result of a single showwarning() call."""
+
+ _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
+ "line")
+
+ def __init__(self, message, category, filename, lineno, file=None,
+ line=None):
+ local_values = locals()
+ for attr in self._WARNING_DETAILS:
+ setattr(self, attr, local_values[attr])
+ self._category_name = category.__name__ if category else None
+
+ def __str__(self):
+ return ("{message : %r, category : %r, filename : %r, lineno : %s, "
+ "line : %r}" % (self.message, self._category_name,
+ self.filename, self.lineno, self.line))
+
+
+ class catch_warnings(object):
+
+ """A context manager that copies and restores the warnings filter upon
+ exiting the context.
+
+ The 'record' argument specifies whether warnings should be captured by a
+ custom implementation of warnings.showwarning() and be appended to a list
+ returned by the context manager. Otherwise None is returned by the context
+ manager. The objects appended to the list are arguments whose attributes
+ mirror the arguments to showwarning().
+
+ The 'module' argument is to specify an alternative module to the module
+ named 'warnings' and imported under that name. This argument is only useful
+ when testing the warnings module itself.
+
+ """
+
+ def __init__(self, record=False, module=None):
+ """Specify whether to record warnings and if an alternative module
+ should be used other than sys.modules['warnings'].
+
+ For compatibility with Python 3.0, please consider all arguments to be
+ keyword-only.
+
+ """
+ self._record = record
+ self._module = sys.modules['warnings'] if module is None else module
+ self._entered = False
+
+ def __repr__(self):
+ args = []
+ if self._record:
+ args.append("record=True")
+ if self._module is not sys.modules['warnings']:
+ args.append("module=%r" % self._module)
+ name = type(self).__name__
+ return "%s(%s)" % (name, ", ".join(args))
+
+ def __enter__(self):
+ if self._entered:
+ raise RuntimeError("Cannot enter %r twice" % self)
+ self._entered = True
+ self._filters = self._module.filters
+ self._module.filters = self._filters[:]
+ self._showwarning = self._module.showwarning
+ if self._record:
+ log = []
+ def showwarning(*args, **kwargs):
+# self._showwarning(*args, **kwargs)
+ log.append(WarningMessage(*args, **kwargs))
+ self._module.showwarning = showwarning
+ return log
+ else:
+ return None
+
+ def __exit__(self, *exc_info):
+ if not self._entered:
+ raise RuntimeError("Cannot exit %r without entering first" % self)
+ self._module.filters = self._filters
+ self._module.showwarning = self._showwarning
+
+#=============================================================================
+# eof
+#=============================================================================
diff --git a/passlib/tests/tox_support.py b/passlib/tests/tox_support.py
index 4c8cee8..ac375e5 100644
--- a/passlib/tests/tox_support.py
+++ b/passlib/tests/tox_support.py
@@ -22,18 +22,33 @@ __all__ = [
#=============================================================================
# main
#=============================================================================
+TH_PATH = "passlib/tests/test_handlers.py"
+
+def do_hash_tests(*args):
+ "return list of hash algorithm tests that match regexes"
+ if not args:
+ print(TH_PATH)
+ return
+ suffix = ''
+ args = list(args)
+ while True:
+ if args[0] == "--method":
+ suffix = '.' + args[1]
+ del args[:2]
+ else:
+ break
+ from passlib.tests import test_handlers
+ names = [TH_PATH + ":" + name + suffix for name in dir(test_handlers)
+ if not name.startswith("_") and any(re.match(arg,name) for arg in args)]
+ print_("\n".join(names))
+ return not names
+
def do_preset_tests(name):
"return list of preset test names"
if name == "django" or name == "django-hashes":
- from passlib.tests import test_handlers
- names = [
- "passlib/tests/test_handlers.py:" + name
- for name in dir(test_handlers)
- if re.match("^django_.*_test$", name)
- ] + ["hex_md5_test"]
+ do_hash_tests("django_.*_test", "hex_md5_test")
if name == "django":
- names.append("passlib/tests/test_ext_django.py")
- print_(" ".join(names))
+ print_("passlib/tests/test_ext_django.py")
else:
raise ValueError("unknown name: %r" % name)
@@ -52,8 +67,7 @@ handlers:
""" % runtime)
def main(cmd, *args):
- func = globals()["do_" + cmd]
- return func(*args)
+ return globals()["do_" + cmd](*args)
if __name__ == "__main__":
import sys
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 77e8933..be3eb61 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -12,32 +12,13 @@ import sys
import tempfile
from passlib.exc import PasslibHashWarning
from passlib.utils.compat import PY27, PY_MIN_32, PY3
-from warnings import warn
-
-try:
- import unittest2 as unittest
- ut_version = 2
-except ImportError:
- import unittest
- if PY27 or PY_MIN_32:
- ut_version = 2
- else:
- # older versions of python will need to install the unittest2
- # backport (named unittest2_3k for 3.0/3.1)
- warn("please install unittest2 for python %d.%d, it will be required "
- "as of passlib 1.7" % sys.version_info[:2])
- ut_version = 1
-
import warnings
from warnings import warn
-
#site
-if ut_version < 2:
- #used to provide replacement skipTest() method
- from nose.plugins.skip import SkipTest
#pkg
from passlib.exc import MissingBackendError
import passlib.registry as registry
+from passlib.tests.backports import TestCase as _TestCase, catch_warnings, skip, skipIf, skipUnless
from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \
classproperty, rng, getrandstr, is_ascii_safe, to_native_str, \
repeat_string
@@ -47,81 +28,119 @@ import passlib.utils.handlers as uh
#local
__all__ = [
#util funcs
- 'enable_option',
- 'Params',
+ 'TEST_MODE',
'set_file', 'get_file',
#unit testing
'TestCase',
'HandlerCase',
- 'enable_backend_case',
- 'create_backend_case',
-
- #flags
- 'gae_env',
]
-#figure out if we're running under GAE...
-#some tests (eg FS related) should be skipped.
- #XXX: is there better way to do this?
+#=========================================================
+# environment detection
+#=========================================================
+# figure out if we're running under GAE;
+# some tests (e.g. FS writing) should be skipped.
+# XXX: is there better way to do this?
try:
import google.appengine
except ImportError:
- gae_env = False
+ GAE = False
else:
- gae_env = True
+ GAE = True
#=========================================================
-#option flags
+# test mode
#=========================================================
-DEFAULT_TESTS = ""
-
-tests = set(
- v.strip()
- for v
- in os.environ.get("PASSLIB_TESTS", DEFAULT_TESTS).lower().split(",")
- )
-
-def enable_option(*names):
- """check if a given test should be included based on the env var.
-
- test flags:
- all-backends test all backends, even the inactive ones
- cover enable minor tweaks to maximize coverage testing
- all run all tests
+_TEST_MODES = ["quick", "default", "full"]
+_test_mode = _TEST_MODES.index(os.environ.get("PASSLIB_TEST_MODE",
+ "default").strip().lower())
+
+def TEST_MODE(min=None, max=None):
+ """check if test for specified mode should be enabled.
+
+ ``"quick"``
+ run the bare minimum tests to ensure functionality.
+ variable-cost hashes are tested at their lowest setting.
+ hash algorithms are only tested against the backend that will
+ be used on the current host. no fuzz testing is done.
+
+ ``"default"``
+ same as ``"quick"``, except: hash algorithms are tested
+ at default levels, and a brief round of fuzz testing is done
+ for each hash.
+
+ ``"full"``
+ extra regression and internal tests are enabled, hash algorithms are tested
+ against all available backends, unavailable ones are mocked whre possible,
+ additional time is devoted to fuzz testing.
"""
- return 'all' in tests or any(name in tests for name in names)
+ if min and _test_mode < _TEST_MODES.index(min):
+ return False
+ if max and _test_mode > _TEST_MODES.index(max):
+ return False
+ return True
#=========================================================
-#misc utility funcs
+# hash object inspection
#=========================================================
-class Params(object):
- "helper to represent params for function call"
+def has_crypt_support(handler):
+ "check if host's crypt() supports this natively"
+ if hasattr(handler, "orig_prefix"):
+ # ignore wrapper classes
+ return False
+ return 'os_crypt' in getattr(handler, "backends", ()) and handler.has_backend("os_crypt")
- @classmethod
- def norm(cls, value):
- if isinstance(value, cls):
- return value
- if isinstance(value, (list,tuple)):
- return cls(*value)
- return cls(**value)
-
- def __init__(self, *args, **kwds):
- self.args = args
- self.kwds = kwds
-
- def render(self, offset=0):
- """render parenthesized parameters"""
- txt = ''
- for a in self.args[offset:]:
- txt += "%r, " % (a,)
- kwds = self.kwds
- for k in sorted(kwds):
- txt += "%s=%r, " % (k, kwds[k])
- if txt.endswith(", "):
- txt = txt[:-2]
- return txt
+def has_relaxed_setting(handler):
+ "check if handler supports 'relaxed' kwd"
+ # FIXME: I've been lazy, should probably just add 'relaxed' kwd
+ # to all handlers that derive from GenericHandler
+
+ # ignore wrapper classes for now.. though could introspec.
+ if hasattr(handler, "orig_prefix"):
+ return False
+
+ return 'relaxed' in handler.setting_kwds or issubclass(handler,
+ uh.GenericHandler)
+
+def has_active_backend(handler):
+ "return active backend for handler, if any"
+ if not hasattr(handler, "get_backend"):
+ return "builtin"
+ try:
+ return handler.get_backend()
+ except MissingBackendError:
+ return None
+
+def is_default_backend(handler, backend):
+ "check if backend is the default for source"
+ try:
+ orig = handler.get_backend()
+ except MissingBackendError:
+ return False
+ try:
+ return handler.set_backend("default") == backend
+ finally:
+ handler.set_backend(orig)
+class temporary_backend(object):
+ "temporarily set handler to specific backend"
+ def __init__(self, handler, backend=None):
+ self.handler = handler
+ self.backend = backend
+
+ def __enter__(self):
+ orig = self._orig = self.handler.get_backend()
+ if self.backend:
+ self.handler.set_backend(self.backend)
+ return orig
+
+ def __exit__(self, *exc_info):
+ self.handler.set_backend(self._orig)
+
+#=========================================================
+# misc helpers
+#=========================================================
def set_file(path, content):
"set file to specified bytes"
if isinstance(content, unicode):
@@ -146,10 +165,29 @@ def tonn(source):
except UnicodeDecodeError:
return source.decode("latin-1")
+def limit(value, lower, upper):
+ if value < lower:
+ return lower
+ elif value > upper:
+ return upper
+ return value
+
+def randintgauss(lower, upper, mu, sigma):
+ "hack used by fuzz testing"
+ return int(limit(rng.normalvariate(mu, sigma), lower, upper))
+
+def get_timer_resolution(timer):
+ def sample():
+ start = cur = timer()
+ while start == cur:
+ cur = timer()
+ return cur-start
+ return min(sample() for _ in range(3))
+
#=========================================================
-#custom test base
+# custom test harness
#=========================================================
-class TestCase(unittest.TestCase):
+class TestCase(_TestCase):
"""passlib-specific test case class
this class adds a number of features to the standard TestCase...
@@ -157,7 +195,6 @@ class TestCase(unittest.TestCase):
* resets warnings filter & registry for every test
* tweaks to message formatting
* __msg__ kwd added to assertRaises()
- * backport of a bunch of unittest2 features
* suite of methods for matching against warnings
"""
#====================================================================
@@ -205,7 +242,7 @@ class TestCase(unittest.TestCase):
# reset warning filters & registry before each test
#----------------------------------------------------------------
- # flag to enable this feature
+ # flag to reset all warning filters & ignore state
resetWarningState = True
def setUp(self):
@@ -213,6 +250,7 @@ class TestCase(unittest.TestCase):
self.setUpWarnings()
def setUpWarnings(self):
+ "helper to init warning filters before subclass setUp()"
if self.resetWarningState:
ctx = reset_warnings()
ctx.__enter__()
@@ -248,154 +286,11 @@ class TestCase(unittest.TestCase):
raise self.failureException(self._formatMessage(msg, std))
#----------------------------------------------------------------
- # null out a bunch of deprecated aliases so I stop using them
- #----------------------------------------------------------------
- assertEquals = assertNotEquals = assertRegexpMatches = None
-
- #====================================================================
- # backport some methods from unittest2
- #====================================================================
- if ut_version < 2:
-
- #----------------------------------------------------------------
- # simplistic backport of addCleanup() framework
- #----------------------------------------------------------------
- _cleanups = None
-
- def addCleanup(self, function, *args, **kwds):
- queue = self._cleanups
- if queue is None:
- queue = self._cleanups = []
- queue.append((function, args, kwds))
-
- def doCleanups(self):
- queue = self._cleanups
- while queue:
- func, args, kwds = queue.pop()
- func(*args, **kwds)
-
- def tearDown(self):
- self.doCleanups()
- unittest.TestCase.tearDown(self)
-
- #----------------------------------------------------------------
- # backport skipTest (requires nose to work)
- #----------------------------------------------------------------
- def skipTest(self, reason):
- raise SkipTest(reason)
-
- #----------------------------------------------------------------
- # backport various assert tests added in unittest2
- #----------------------------------------------------------------
- def assertIs(self, real, correct, msg=None):
- if real is not correct:
- std = "got %r, expected would be %r" % (real, correct)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
-
- def assertIsNot(self, real, correct, msg=None):
- if real is correct:
- std = "got %r, expected would not be %r" % (real, correct)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
-
- def assertIsInstance(self, obj, klass, msg=None):
- if not isinstance(obj, klass):
- std = "got %r, expected instance of %r" % (obj, klass)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
-
- def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None):
- """Fail if the two objects are unequal as determined by their
- difference rounded to the given number of decimal places
- (default 7) and comparing to zero, or by comparing that the
- between the two objects is more than the given delta.
-
- Note that decimal places (from zero) are usually not the same
- as significant digits (measured from the most signficant digit).
-
- If the two objects compare equal then they will automatically
- compare almost equal.
- """
- if first == second:
- # shortcut
- return
- if delta is not None and places is not None:
- raise TypeError("specify delta or places not both")
-
- if delta is not None:
- if abs(first - second) <= delta:
- return
-
- standardMsg = '%s != %s within %s delta' % (repr(first),
- repr(second),
- repr(delta))
- else:
- if places is None:
- places = 7
-
- if round(abs(second-first), places) == 0:
- return
-
- standardMsg = '%s != %s within %r places' % (repr(first),
- repr(second),
- places)
- msg = self._formatMessage(msg, standardMsg)
- raise self.failureException(msg)
-
- def assertLess(self, left, right, msg=None):
- if left >= right:
- std = "%r not less than %r" % (left, right)
- raise self.failureException(self._formatMessage(msg, std))
-
- def assertGreaterEqual(self, left, right, msg=None):
- if left < right:
- std = "%r less than %r" % (left, right)
- raise self.failureException(self._formatMessage(msg, std))
-
- def assertIn(self, elem, container, msg=None):
- if elem not in container:
- std = "%r not found in %r" % (elem, container)
- raise self.failureException(self._formatMessage(msg, std))
-
- def assertNotIn(self, elem, container, msg=None):
- if elem in container:
- std = "%r unexpectedly in %r" % (elem, container)
- raise self.failureException(self._formatMessage(msg, std))
-
- #----------------------------------------------------------------
- # override some unittest1 methods to support _formatMessage
- #----------------------------------------------------------------
- def assertEqual(self, real, correct, msg=None):
- if real != correct:
- std = "got %r, expected would equal %r" % (real, correct)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
-
- def assertNotEqual(self, real, correct, msg=None):
- if real == correct:
- std = "got %r, expected would not equal %r" % (real, correct)
- msg = self._formatMessage(msg, std)
- raise self.failureException(msg)
-
- #----------------------------------------------------------------
- # backport assertRegex() alias from 3.2 to 2.7/3.1
+ # forbid a bunch of deprecated aliases so I stop using them
#----------------------------------------------------------------
- if not hasattr(unittest.TestCase, "assertRegex"):
- if hasattr(unittest.TestCase, "assertRegexpMatches"):
- # was present in 2.7/3.1 under name assertRegexpMatches
- assertRegex = unittest.TestCase.assertRegexpMatches
- else:
- # 3.0 and <= 2.6 didn't have this method at all
- def assertRegex(self, text, expected_regex, msg=None):
- """Fail the test unless the text matches the regular expression."""
- if isinstance(expected_regex, base_string_types):
- assert expected_regex, "expected_regex must not be empty."
- expected_regex = re.compile(expected_regex)
- if not expected_regex.search(text):
- msg = msg or "Regex didn't match: "
- std = '%r not found in %r' % (msg, expected_regex.pattern, text)
- raise self.failureException(self._formatMessage(msg, std))
+ def assertEquals(self, *a, **k):
+ raise AssertionError("this alias is deprecated by unittest2")
+ assertNotEquals = assertRegexMatches = assertEquals
#============================================================
# custom methods for matching warnings
@@ -407,8 +302,10 @@ class TestCase(unittest.TestCase):
lineno=None,
msg=None,
):
- "check if WarningMessage instance (as returned by catch_warnings) matches parameters"
-
+ """check if warning matches specified parameters.
+ 'warning' is the instance of Warning to match against;
+ can also be instance of WarningMessage (as returned by catch_warnings).
+ """
# check input type
if hasattr(warning, "category"):
# resolve WarningMessage -> Warning, but preserve original
@@ -446,11 +343,31 @@ class TestCase(unittest.TestCase):
"WarningMessage instance")
self.assertEqual(wmsg.lineno, lineno, msg)
- def assertWarningList(self, wlist, desc=None, msg=None):
+ class _AssertWarningList(catch_warnings):
+ """context manager for assertWarningList()"""
+ def __init__(self, case, **kwds):
+ self.case = case
+ self.kwds = kwds
+ self.__super = super(TestCase._AssertWarningList, self)
+ self.__super.__init__(record=True)
+
+ def __enter__(self):
+ self.log = self.__super.__enter__()
+
+ def __exit__(self, *exc_info):
+ self.__super.__exit__(*exc_info)
+ if not exc_info:
+ self.case.assertWarningList(self.log, **self.kwds)
+
+ def assertWarningList(self, wlist=None, desc=None, msg=None):
"""check that warning list (e.g. from catch_warnings) matches pattern"""
+ if desc is None:
+ assert wlist is not None
+ return self._AssertWarningList(self, desc=wlist, msg=msg)
# TODO: make this display better diff of *which* warnings did not match
+ assert desc is not None
if not isinstance(desc, (list,tuple)):
- desc = [] if desc is None else [desc]
+ desc = [desc]
for idx, entry in enumerate(desc):
if isinstance(entry, str):
entry = dict(message_re=entry)
@@ -470,9 +387,11 @@ class TestCase(unittest.TestCase):
(len(desc), len(wlist), self._formatWarningList(wlist), desc)
raise self.failureException(self._formatMessage(msg, std))
- def consumeWarningList(self, wlist, *args, **kwds):
- """assertWarningList() variant that clears list afterwards"""
- self.assertWarningList(wlist, *args, **kwds)
+ def consumeWarningList(self, wlist, desc=None, *args, **kwds):
+ """[deprecated] assertWarningList() variant that clears list afterwards"""
+ if desc is None:
+ desc = []
+ self.assertWarningList(wlist, desc, *args, **kwds)
del wlist[:]
def _formatWarning(self, entry):
@@ -491,23 +410,8 @@ class TestCase(unittest.TestCase):
return "[%s]" % ", ".join(self._formatWarning(entry) for entry in wlist)
#============================================================
- # misc custom methods
+ # capability tests
#============================================================
- def assertFunctionResults(self, func, cases):
- """helper for running through function calls.
-
- func should be the function to call.
- cases should be list of Param instances,
- where first position argument is expected return value,
- and remaining args and kwds are passed to function.
- """
- for elem in cases:
- elem = Params.norm(elem)
- correct = elem.args[0]
- result = func(*elem.args[1:], **elem.kwds)
- msg = "error for case %r:" % (elem.render(1),)
- self.assertEqual(result, correct, msg)
-
def require_stringprep(self):
"helper to skip test if stringprep is missing"
from passlib.utils import stringprep
@@ -515,6 +419,39 @@ class TestCase(unittest.TestCase):
from passlib.utils import _stringprep_missing_reason
raise self.skipTest("not available - stringprep module is " +
_stringprep_missing_reason)
+
+ def require_TEST_MODE(self, level):
+ "skip test for all PASSLIB_TEST_MODE values below <level>"
+ if not TEST_MODE(level):
+ raise self.skipTest("requires >= %r test mode" % level)
+
+ def require_writeable_filesystem(self):
+ "skip test if writeable FS not available"
+ if GAE:
+ return self.skipTest("GAE doesn't offer read/write filesystem access")
+
+ #============================================================
+ # other
+ #============================================================
+ _mktemp_queue = None
+
+ def mktemp(self, *args, **kwds):
+ "create temp file that's cleaned up at end of test"
+ self.require_writeable_filesystem()
+ fd, path = tempfile.mkstemp(*args, **kwds)
+ os.close(fd)
+ queue = self._mktemp_queue
+ if queue is None:
+ queue = self._mktemp_queue = []
+ def cleaner():
+ for path in queue:
+ if os.path.exists(path):
+ os.remove(path)
+ del queue[:]
+ self.addCleanup(cleaner)
+ queue.append(path)
+ return path
+
#============================================================
#eoc
#============================================================
@@ -542,17 +479,17 @@ class HandlerCase(TestCase):
(or :class:`unittest2.TestCase` if available).
"""
#=========================================================
- # attrs to be filled in by subclass for testing specific handler
+ # class attrs - should be filled in by subclass
#=========================================================
#--------------------------------------------------
# handler setup
#--------------------------------------------------
- # specify handler object here (required)
+ # handler class to test [required]
handler = None
- # run tests against specific backend (optional, when applicable)
+ # if set, run tests against specified backend
backend = None
#--------------------------------------------------
@@ -618,34 +555,41 @@ class HandlerCase(TestCase):
# flag/hack to filter PasslibHashWarning issued by test_72_configs()
filter_config_warnings = False
+ # forbid certain characters in passwords
+ @classproperty
+ def forbidden_characters(cls):
+ # anything that supports crypt() interface should forbid null chars,
+ # since crypt() uses null-terminated strings.
+ if 'os_crypt' in getattr(cls.handler, "backends", ()):
+ return b("\x00")
+ return None
+
#=========================================================
- # alg interface helpers - allows subclass to overide how
- # default tests invoke the handler (eg for context_kwds)
+ # internal class attrs
#=========================================================
+ __unittest_skip = True
- def do_encrypt(self, secret, **kwds):
- "call handler's encrypt method with specified options"
- return self.handler.encrypt(secret, **kwds)
-
- def do_verify(self, secret, hash, **kwds):
- "call handler's verify method"
- return self.handler.verify(secret, hash, **kwds)
-
- def do_identify(self, hash):
- "call handler's identify method"
- return self.handler.identify(hash)
-
- def do_genconfig(self, **kwds):
- "call handler's genconfig method with specified options"
- return self.handler.genconfig(**kwds)
+ @property
+ def descriptionPrefix(self):
+ handler = self.handler
+ name = handler.name
+ if hasattr(handler, "get_backend"):
+ name += " (%s backend)" % (handler.get_backend(),)
+ return name
- def do_genhash(self, secret, config, **kwds):
- "call handler's genhash method with specified options"
- return self.handler.genhash(secret, config, **kwds)
+ #=========================================================
+ # internal instance attrs
+ #=========================================================
+ # indicates safe_crypt() has been patched to use another backend of handler.
+ using_patched_crypt = False
#=========================================================
- # support
+ # support methods
#=========================================================
+
+ #------------------------------------------------------
+ # configuration helpers
+ #------------------------------------------------------
@property
def supports_config_string(self):
return self.do_genconfig() is not None
@@ -665,6 +609,9 @@ class HandlerCase(TestCase):
known = list(self.iter_known_hashes())
return rng.choice(known)
+ #------------------------------------------------------
+ # test helpers
+ #------------------------------------------------------
def check_verify(self, secret, hash, msg=None, negate=False):
"helper to check verify() outcome, honoring is_disabled_handler"
result = self.do_verify(secret, hash)
@@ -688,33 +635,118 @@ class HandlerCase(TestCase):
self.assertIsInstance(result, str,
"%s() failed to return native string: %r" % (func_name, result,))
- #=========================================================
- # internal class attrs
- #=========================================================
- __unittest_skip = True
-
- @property
- def descriptionPrefix(self):
+ #------------------------------------------------------
+ # PasswordHash helpers - wraps all calls to PasswordHash api,
+ # so that subclasses can fill in defaults and account for other specialized behavior
+ #------------------------------------------------------
+ def populate_settings(self, kwds):
+ "subclassable method to populate default settings"
+ # use lower rounds settings for certain test modes
handler = self.handler
- name = handler.name
- if hasattr(handler, "get_backend"):
- name += " (%s backend)" % (handler.get_backend(),)
- return name
+ if 'rounds' in handler.setting_kwds and 'rounds' not in kwds:
+ mn = handler.min_rounds
+ df = handler.default_rounds
+ if TEST_MODE(max="quick"):
+ # use minimum rounds for quick mode
+ kwds['rounds'] = max(3, mn)
+ else:
+ # use default/16 otherwise
+ factor = 3
+ if getattr(handler, "rounds_cost", None) == "log2":
+ df -= factor
+ else:
+ df = df//(1<<factor)
+ kwds['rounds'] = max(3, mn, df)
- #=========================================================
- # internal instance attrs
- #=========================================================
- # indicates safe_crypt() has been patched to use another backend of handler.
- using_patched_crypt = False
+ def populate_context(self, secret, kwds):
+ "subclassable method allowing 'secret' to be encode context kwds"
+ return secret
- # backup of original utils.os_crypt before it was patched.
- _orig_crypt = None
+ def do_encrypt(self, secret, **kwds):
+ "call handler's encrypt method with specified options"
+ secret = self.populate_context(secret, kwds)
+ self.populate_settings(kwds)
+ return self.handler.encrypt(secret, **kwds)
+
+ def do_verify(self, secret, hash, **kwds):
+ "call handler's verify method"
+ secret = self.populate_context(secret, kwds)
+ return self.handler.verify(secret, hash, **kwds)
+
+ def do_identify(self, hash):
+ "call handler's identify method"
+ return self.handler.identify(hash)
+
+ def do_genconfig(self, **kwds):
+ "call handler's genconfig method with specified options"
+ self.populate_settings(kwds)
+ return self.handler.genconfig(**kwds)
- # backup of original backend before test started
- _orig_backend = None
+ def do_genhash(self, secret, config, **kwds):
+ "call handler's genhash method with specified options"
+ secret = self.populate_context(secret, kwds)
+ return self.handler.genhash(secret, config, **kwds)
+
+ #------------------------------------------------------
+ # automatically generate subclasses for testing specific backends,
+ # and other backend helpers
+ #------------------------------------------------------
+ @classmethod
+ def _enable_backend_case(cls, backend):
+ "helper for create_backend_cases(); returns reason to skip backend, or None"
+ handler = cls.handler
+ if not is_default_backend(handler, backend) and not TEST_MODE("full"):
+ return "only default backend is being tested"
+ if handler.has_backend(backend):
+ return None
+ if handler.name == "bcrypt" and backend == "builtin" and TEST_MODE("full"):
+ # this will be auto-enabled under TEST_MODE 'full'.
+ return None
+ if backend == "os_crypt":
+ if cls.find_crypt_replacement() and TEST_MODE("full"):
+ #in this case, HandlerCase will monkeypatch os_crypt
+ #to use another backend, just so we can test os_crypt fully.
+ return None
+ from passlib.utils import has_crypt
+ if has_crypt:
+ return "hash not supported by os crypt()"
+ return "backend not available"
+
+ @classmethod
+ def create_backend_cases(cls, backends, module=None):
+ handler = cls.handler
+ name = handler.name
+ assert hasattr(handler, "backends"), "handler must support uh.HasManyBackends protocol"
+ for backend in backends:
+ assert backend in handler.backends, "unknown backend: %r" % (backend,)
+ bases = (cls,)
+ if backend == "os_crypt":
+ bases += (OsCryptMixin,)
+ subcls = type(
+ "%s_%s_test" % (name, backend),
+ bases,
+ dict(
+ descriptionPrefix = "%s (%s backend)" % (name, backend),
+ backend = backend,
+ __module__= module or cls.__module__,
+ )
+ )
+ skip_reason = cls._enable_backend_case(backend)
+ if skip_reason:
+ subcls = skip(skip_reason)(subcls)
+ yield subcls
+
+ @classmethod
+ def find_crypt_replacement(cls):
+ "find other backend which can be used to mock the os_crypt backend"
+ handler = cls.handler
+ for name in handler.backends:
+ if name != "os_crypt" and handler.has_backend(name):
+ return name
+ return None
#=========================================================
- # setup / cleanup
+ # setup
#=========================================================
def setUp(self):
super(HandlerCase, self).setUp()
@@ -1076,7 +1108,7 @@ class HandlerCase(TestCase):
#
# should accept too-large salt in relaxed mode
#
- if _has_relaxed_setting(handler):
+ if has_relaxed_setting(handler):
with catch_warnings(record=True): # issues passlibhandlerwarning
c2 = self.do_genconfig(salt=s2, relaxed=True)
self.assertEqual(c2, c1)
@@ -1253,7 +1285,7 @@ class HandlerCase(TestCase):
# check constructor validates ident correctly.
handler = cls
hash = self.get_sample_hash()[1]
- kwds = _hobj_to_dict(handler.from_string(hash))
+ kwds = handler.parsehash(hash)
del kwds['ident']
# ... accepts good ident
@@ -1359,10 +1391,24 @@ class HandlerCase(TestCase):
self.assertRaises(PasswordSizeError, self.do_encrypt, secret)
self.assertRaises(PasswordSizeError, self.do_verify, secret, hash)
+ def test_64_forbidden_chars(self):
+ "test forbidden characters not allowed in password"
+ chars = self.forbidden_characters
+ if not chars:
+ raise self.skipTest("none listed")
+ base = u('stub')
+ if isinstance(chars, bytes):
+ from passlib.utils.compat import iter_byte_chars
+ chars = iter_byte_chars(chars)
+ base = base.encode("ascii")
+ for c in chars:
+ self.assertRaises(ValueError, self.do_encrypt, base + c + base)
+
#==============================================================
# check identify(), verify(), genhash() against test vectors
#==============================================================
def is_secret_8bit(self, secret):
+ secret = self.populate_context(secret, {})
return not is_ascii_safe(secret)
def test_70_hashes(self):
@@ -1582,13 +1628,13 @@ class HandlerCase(TestCase):
self.do_identify('\xe2\x82\xac\xc2\xa5$') # utf-8
self.do_identify('abc\x91\x00') # non-utf8
- #---------------------------------------------------------
+ #=========================================================
# fuzz testing
- #---------------------------------------------------------
+ #=========================================================
"""the following attempts to perform some basic fuzz testing
of the handler, based on whatever information can be found about it.
it does as much as it can within a fixed amount of time
- (defaults to 1 second, but can be overridden via $PASSLIB_TESTS_FUZZ_TIME).
+ (defaults to 1 second, but can be overridden via $PASSLIB_TEST_FUZZ_TIME).
it tests the following:
* randomly generated passwords including extended unicode chars
@@ -1607,7 +1653,13 @@ class HandlerCase(TestCase):
@property
def max_fuzz_time(self):
- return float(os.environ.get("PASSLIB_TESTS_FUZZ_TIME") or 1)
+ if TEST_MODE(max="quick"):
+ default = 0
+ elif TEST_MODE(max="default"):
+ default = 1
+ else:
+ default = 5
+ return float(os.environ.get("PASSLIB_TEST_FUZZ_TIME") or default)
def test_77_fuzz_input(self):
"""test random passwords and options"""
@@ -1619,6 +1671,8 @@ class HandlerCase(TestCase):
handler = self.handler
disabled = self.is_disabled_handler
max_time = self.max_fuzz_time
+ if max_time <= 0:
+ raise self.skipTest("disabled for this test mode")
verifiers = self.get_fuzz_verifiers()
def vname(v):
return (v.__doc__ or v.__name__).splitlines()[0]
@@ -1680,7 +1734,7 @@ class HandlerCase(TestCase):
verifiers.append(func)
# create verifiers for any other available backends
- if hasattr(handler, "backends") and enable_option("all-backends"):
+ if hasattr(handler, "backends") and TEST_MODE("full"):
def maker(backend):
def func(secret, hash):
with temporary_backend(handler, backend):
@@ -1710,11 +1764,9 @@ class HandlerCase(TestCase):
return True
def fuzz_verifier_crypt(self):
- # test againt OS crypt()
- # NOTE: skipping this if using_patched_crypt since _has_crypt_support()
- # will return false positive in that case.
+ "test results against OS crypt()"
handler = self.handler
- if self.using_patched_crypt or not _has_crypt_support(handler):
+ if self.using_patched_crypt or not has_crypt_support(handler):
return None
from crypt import crypt
def check_crypt(secret, hash):
@@ -1778,6 +1830,8 @@ class HandlerCase(TestCase):
else:
lower = handler.min_rounds #max(default*.5, handler.min_rounds)
upper = min(default*2, handler.max_rounds)
+ if TEST_MODE(max="quick"):
+ upper = min(2, lower)
return randintgauss(lower, upper, default, default*.5)
def get_fuzz_salt_size(self):
@@ -1798,11 +1852,12 @@ class HandlerCase(TestCase):
return rng.choice(handler.ident_values)
#=========================================================
- # test 8x - mixin tests
- # test 9x - handler-specific tests
# eoc
#=========================================================
+#=================================================================
+# HandlerCase mixins providing additional tests for certain hashes
+#=================================================================
class OsCryptMixin(HandlerCase):
"""helper used by create_backend_case() which adds additional features
to test the os_crypt backend.
@@ -1850,7 +1905,7 @@ class OsCryptMixin(HandlerCase):
as possible.
"""
handler = self.handler
- alt_backend = _find_alternate_backend(handler, "os_crypt")
+ alt_backend = self.find_crypt_replacement()
if not alt_backend:
raise AssertionError("handler has no available backends!")
import passlib.utils as mod
@@ -1900,7 +1955,7 @@ class OsCryptMixin(HandlerCase):
# set safe_crypt to return None
setter = self._use_mock_crypt()
setter(None)
- if _find_alternate_backend(self.handler, "os_crypt"):
+ if self.find_crypt_replacement():
# handler should have a fallback to use
h1 = self.do_encrypt("stub")
h2 = self.do_genhash("stub", h1)
@@ -1999,12 +2054,7 @@ class UserHandlerMixin(HandlerCase):
#=========================================================
# override test helpers
#=========================================================
-
- def is_secret_8bit(self, secret):
- secret = self._insert_user({}, secret)
- return not is_ascii_safe(secret)
-
- def _insert_user(self, kwds, secret):
+ def populate_context(self, secret, kwds):
"insert username into kwds"
if isinstance(secret, tuple):
secret, user = secret
@@ -2016,18 +2066,6 @@ class UserHandlerMixin(HandlerCase):
kwds['user'] = user
return secret
- def do_encrypt(self, secret, **kwds):
- secret = self._insert_user(kwds, secret)
- return self.handler.encrypt(secret, **kwds)
-
- def do_verify(self, secret, hash, **kwds):
- secret = self._insert_user(kwds, secret)
- return self.handler.verify(secret, hash, **kwds)
-
- def do_genhash(self, secret, config, **kwds):
- secret = self._insert_user(kwds, secret)
- return self.handler.genhash(secret, config, **kwds)
-
#=========================================================
# modify fuzz testing
#=========================================================
@@ -2044,251 +2082,44 @@ class UserHandlerMixin(HandlerCase):
# eoc
#=========================================================
-#=========================================================
-#backend test helpers
-#=========================================================
-def _enable_backend_case(handler, backend):
- "helper to check if testcase should be enabled for the specified backend"
- assert backend in handler.backends, "unknown backend: %r" % (backend,)
- if enable_option("all-backends") or _is_default_backend(handler, backend):
- if handler.has_backend(backend):
- return True, None
- from passlib.utils import has_crypt
- if backend == "os_crypt" and has_crypt:
- if enable_option("cover") and _find_alternate_backend(handler, "os_crypt"):
- #in this case, HandlerCase will monkeypatch os_crypt
- #to use another backend, just so we can test os_crypt fully.
- return True, None
- else:
- return False, "hash not supported by os crypt()"
- else:
- return False, "backend not available"
- else:
- return False, "only default backend being tested"
+class EncodingHandlerMixin(HandlerCase):
+ """helper for handlers w/ 'encoding' context kwd; mixin for HandlerCase
-def _is_default_backend(handler, name):
- "check if backend is the default for handler"
- try:
- orig = handler.get_backend()
- except MissingBackendError:
- return False
- try:
- return handler.set_backend("default") == name
- finally:
- handler.set_backend(orig)
-
-def _find_alternate_backend(handler, ignore):
- "helper to check if alternate backend is available"
- for name in handler.backends:
- if name != ignore and handler.has_backend(name):
- return name
- return None
-
-def _has_crypt_support(handler):
- "check if host OS' crypt() supports this natively"
- # ignore wrapper classes
- if hasattr(handler, "orig_prefix"):
- return False
- # os crypt support?
- return hasattr(handler, "backends") and \
- 'os_crypt' in handler.backends and \
- handler.has_backend("os_crypt")
-
-def _has_relaxed_setting(handler):
- # FIXME: I've been lazy, should probably just add 'relaxed' kwd
- # to all handlers that derive from GenericHandler
-
- # ignore wrapper classes for now.. though could introspec.
- if hasattr(handler, "orig_prefix"):
- return False
-
- return 'relaxed' in handler.setting_kwds or issubclass(handler,
- uh.GenericHandler)
-
-def _hobj_to_dict(hobj):
- "hack to convert handler instance to dict"
- # FIXME: would be good to distinguish config-string keywords
- # from generation options (e.g. salt size) in programmatic manner.
- exclude_keys = ["salt_size", "relaxed"]
- return dict(
- (key, getattr(hobj, key))
- for key in hobj.setting_kwds
- if key not in exclude_keys
- )
-
-def create_backend_case(base_class, backend, module=None):
- "create a test case for specific backend of a multi-backend handler"
- #get handler, figure out if backend should be tested
- handler = base_class.handler
- assert hasattr(handler, "backends"), "handler must support uh.HasManyBackends protocol"
- enable, skip_reason = _enable_backend_case(handler, backend)
-
- #UT1 doesn't support skipping whole test cases, so we just return None.
- if not enable and ut_version < 2:
- return None
-
- # pick bases
- bases = (base_class,)
- if backend == "os_crypt":
- bases += (OsCryptMixin,)
-
- # create subclass to test backend
- backend_class = type(
- "%s_%s" % (backend, handler.name),
- bases,
- dict(
- descriptionPrefix = "%s (%s backend)" % (handler.name, backend),
- backend = backend,
- __module__= module or base_class.__module__,
- )
- )
-
- if not enable:
- backend_class = unittest.skip(skip_reason)(backend_class)
-
- return backend_class
-
-#=========================================================
-#misc helpers
-#=========================================================
-def limit(value, lower, upper):
- if value < lower:
- return lower
- elif value > upper:
- return upper
- return value
-
-def randintgauss(lower, upper, mu, sigma):
- "hack used by fuzz testing"
- return int(limit(rng.normalvariate(mu, sigma), lower, upper))
-
-class temporary_backend(object):
- "temporarily set handler to specific backend"
- def __init__(self, handler, backend=None):
- self.handler = handler
- self.backend = backend
-
- def __enter__(self):
- orig = self._orig = self.handler.get_backend()
- if self.backend:
- self.handler.set_backend(self.backend)
- return orig
-
- def __exit__(self, *exc_info):
- self.handler.set_backend(self._orig)
+ this overrides the HandlerCase test harness methods
+ so that an encoding can be inserted to encrypt/verify
+ calls by passing in a pair of strings as the password
+ will be interpreted as (secret,encoding)
+ """
+ #=========================================================
+ # instance attrs
+ #=========================================================
+ __unittest_skip = True
-#=========================================================
-#helper for creating temp files - all cleaned up when prog exits
-#=========================================================
-tmp_files = []
+ # restrict stock passwords & fuzz alphabet to latin-1,
+ # so different encodings can be tested safely.
+ stock_passwords = [
+ u("test"),
+ b("test"),
+ u("\u00AC\u00BA"),
+ ]
-def _clean_tmp_files():
- for path in tmp_files:
- if os.path.exists(path):
- os.remove(path)
-atexit.register(_clean_tmp_files)
+ fuzz_password_alphabet = u('qwerty1234<>.@*#! \u00AC')
-def mktemp(*args, **kwds):
- fd, path = tempfile.mkstemp(*args, **kwds)
- tmp_files.append(path)
- os.close(fd)
- return path
+ def populate_context(self, secret, kwds):
+ "insert encoding into kwds"
+ if isinstance(secret, tuple):
+ secret, encoding = secret
+ kwds.setdefault('encoding', encoding)
+ return secret
+ #=========================================================
+ # eoc
+ #=========================================================
#=============================================================================
# warnings helpers
#=============================================================================
-
-# make sure catch_warnings() is available
-try:
- from warnings import catch_warnings
-except ImportError:
- #catch_warnings wasn't added until py26.
- #this adds backported copy from py26's stdlib
- #so we can use it under py25.
-
- class WarningMessage(object):
-
- """Holds the result of a single showwarning() call."""
-
- _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
- "line")
-
- def __init__(self, message, category, filename, lineno, file=None,
- line=None):
- local_values = locals()
- for attr in self._WARNING_DETAILS:
- setattr(self, attr, local_values[attr])
- self._category_name = category.__name__ if category else None
-
- def __str__(self):
- return ("{message : %r, category : %r, filename : %r, lineno : %s, "
- "line : %r}" % (self.message, self._category_name,
- self.filename, self.lineno, self.line))
-
-
- class catch_warnings(object):
-
- """A context manager that copies and restores the warnings filter upon
- exiting the context.
-
- The 'record' argument specifies whether warnings should be captured by a
- custom implementation of warnings.showwarning() and be appended to a list
- returned by the context manager. Otherwise None is returned by the context
- manager. The objects appended to the list are arguments whose attributes
- mirror the arguments to showwarning().
-
- The 'module' argument is to specify an alternative module to the module
- named 'warnings' and imported under that name. This argument is only useful
- when testing the warnings module itself.
-
- """
-
- def __init__(self, record=False, module=None):
- """Specify whether to record warnings and if an alternative module
- should be used other than sys.modules['warnings'].
-
- For compatibility with Python 3.0, please consider all arguments to be
- keyword-only.
-
- """
- self._record = record
- self._module = sys.modules['warnings'] if module is None else module
- self._entered = False
-
- def __repr__(self):
- args = []
- if self._record:
- args.append("record=True")
- if self._module is not sys.modules['warnings']:
- args.append("module=%r" % self._module)
- name = type(self).__name__
- return "%s(%s)" % (name, ", ".join(args))
-
- def __enter__(self):
- if self._entered:
- raise RuntimeError("Cannot enter %r twice" % self)
- self._entered = True
- self._filters = self._module.filters
- self._module.filters = self._filters[:]
- self._showwarning = self._module.showwarning
- if self._record:
- log = []
- def showwarning(*args, **kwargs):
-# self._showwarning(*args, **kwargs)
- log.append(WarningMessage(*args, **kwargs))
- self._module.showwarning = showwarning
- return log
- else:
- return None
-
- def __exit__(self, *exc_info):
- if not self._entered:
- raise RuntimeError("Cannot exit %r without entering first" % self)
- self._module.filters = self._filters
- self._module.showwarning = self._showwarning
-
class reset_warnings(catch_warnings):
- "catch_warnings() wrapper which clears warning registry & filters"
+ """catch_warnings() wrapper which clears warning registry & filters"""
def __init__(self, reset_filter="always", reset_registry=".*", **kwds):
super(reset_warnings, self).__init__(**kwds)
self._reset_filter = reset_filter
diff --git a/passlib/utils/compat.py b/passlib/utils/compat.py
index 71f41a7..94ec9bb 100644
--- a/passlib/utils/compat.py
+++ b/passlib/utils/compat.py
@@ -9,6 +9,9 @@ PY_MAX_25 = sys.version_info < (2,6) # py 2.5 or earlier
PY27 = sys.version_info[:2] == (2,7) # supports last 2.x release
PY_MIN_32 = sys.version_info >= (3,2) # py 3.2 or later
+# __dir__() added in py2.6
+SUPPORTS_DIR_METHOD = not PY_MAX_25
+
#=============================================================================
# figure out what VM we're running
#=============================================================================
@@ -137,6 +140,11 @@ if PY3:
assert isinstance(s, bytes)
return s
+ def iter_byte_chars(s):
+ assert isinstance(s, bytes)
+ # FIXME: there has to be a better way to do this
+ return (bytes([c]) for c in s)
+
else:
def uascii_to_str(s):
assert isinstance(s, unicode)
@@ -165,6 +173,10 @@ else:
assert isinstance(s, bytes)
return (ord(c) for c in s)
+ def iter_byte_chars(s):
+ assert isinstance(s, bytes)
+ return s
+
add_doc(uascii_to_str, "helper to convert ascii unicode -> native str")
add_doc(bascii_to_str, "helper to convert ascii bytes -> native str")
add_doc(str_to_uascii, "helper to convert ascii native str -> unicode")
@@ -178,7 +190,8 @@ add_doc(str_to_bascii, "helper to convert ascii native str -> bytes")
# byte_elem_value -- function to convert byte element to integer -- a noop under PY3
-add_doc(iter_byte_values, "helper to iterate over byte values in byte string")
+add_doc(iter_byte_values, "iterate over byte string as sequence of ints 0-255")
+add_doc(iter_byte_chars, "iterate over byte string as sequence of 1-byte strings")
#=============================================================================
# numeric
diff --git a/tox.ini b/tox.ini
index 3d24abf..e057f47 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,27 +1,75 @@
+#===========================================================================
+# NOTES
+# =====
+#
+# PASSLIB_TEST_MODE:
+#
+# "quick"
+# run the bare minimum tests to ensure functionality.
+# variable-cost hashes are tested at their lowest setting.
+# hash algorithms are only tested against the backend that will
+# be used on the current host. no fuzz testing is done.
+#
+# "default"
+# same as ``"quick"``, except: hash algorithms are tested
+# at default levels, and a brief round of fuzz testing is done
+# for each hash.
+#
+# "full"
+# extra regression and internal tests are enabled, hash algorithms are tested
+# against all available backends, unavailable ones are mocked whre possible,
+# additional time is devoted to fuzz testing.
+#
+# testing of m2crypto integration - done in py27 test
+#
+# testing of django integration - split across various cpython tests:
+# py25 - tests django 1.3
+# py26 - tests no django
+# py27 - tests latest django
+#
+# testing of bcrypt backends - split across various cpython tests:
+# py25 - tests builtin bcrypt
+# py27 - tests py-bcrypt, bcryptor
+#===========================================================================
+
+#===========================================================================
+# global config
+#===========================================================================
[tox]
minversion=1.0
-envlist = py27,py32,py25,py26,py31,pypy15,pypy16,pypy17,jython,gae25,gae27
+envlist = py27,py32,py25,py26,py31,pypy15,pypy18,jython,gae25,gae27
#===========================================================================
# stock CPython VMs
#===========================================================================
[testenv]
setenv =
- PASSLIB_TESTS = all
- PASSLIB_TESTS_FUZZ_TIME = 10
+ PASSLIB_TEST_MODE = full
+ PASSLIB_TEST_FUZZ_TIME = 10
changedir = {envdir}
commands =
nosetests {posargs:passlib.tests}
deps =
nose
+ coverage
unittest2
+[testenv:py25]
+deps =
+ nose
+ coverage
+ # unittest2 omitted, to test backport code
+ django<1.4
+
[testenv:py27]
deps =
nose
+ coverage
unittest2
py-bcrypt
bcryptor
+ django
+ M2Crypto
[testenv:py31]
deps =
@@ -31,41 +79,30 @@ deps =
[testenv:py32]
deps =
nose
+ coverage
unittest2py3k
#===========================================================================
-# PyPy VM - all target Python 2.7
+# PyPy VM - all target Python 2.7; supporting 1.5-1.8
#===========================================================================
[testenv:pypy15]
basepython = pypy1.5
-[testenv:pypy16]
-basepython = pypy1.6
-
-[testenv:pypy17]
-basepython = pypy1.7
-setenv =
- PASSLIB_TESTS = all
- PASSLIB_TESTS_FUZZ_TIME = 10
- # this is only interpreter when builtin bcrypt backend
- # isn't punitively slow
- PASSLIB_BUILTIN_BCRYPT = enable
-
-commands =
- nosetests {posargs:passlib.tests}
+[testenv:pypy18]
+basepython = pypy1.8
#===========================================================================
# Jython - no special directives, currently same as py25
#===========================================================================
#===========================================================================
-# Google App Engine
+# Google App Engine integration
#===========================================================================
[testenv:gae25]
basepython = python2.5
deps =
- nose
# FIXME: getting all kinds of errors when using nosegae 0.2.0 :(
+ nose
nosegae==0.1.9
unittest2
changedir = {envdir}/lib/python2.5/site-packages
@@ -73,15 +110,15 @@ commands =
# setup custom app.yaml so GAE can run
python -m passlib.tests.tox_support setup_gae . python
- # have to run without sandbox for now, something in nose+GAE+virtualenv
- # won't play nice with eachother.
+ # FIXME: have to run using --without-sandbox for now,
+ # something in nose+GAE+virtualenv won't play nice with eachother.
nosetests --with-gae --without-sandbox {posargs:passlib/tests}
[testenv:gae27]
basepython = python2.7
deps =
- nose
# FIXME: getting all kinds of errors when using nosegae 0.2.0 :(
+ nose
nosegae==0.1.9
unittest2
changedir = {envdir}/lib/python2.7/site-packages
@@ -89,8 +126,8 @@ commands =
# setup custom app.yaml so GAE can run
python -m passlib.tests.tox_support setup_gae . python27
- # have to run without sandbox for now, something in nose/GAE/virtualenv
- # won't play nice with eachother.
+ # FIXME: have to run using --without-sandbox for now,
+ # something in nose+GAE+virtualenv won't play nice with eachother.
nosetests --with-gae --without-sandbox {posargs:passlib/tests}
#===========================================================================