summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2013-04-11 23:59:32 +0200
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2013-04-11 23:59:32 +0200
commit54971381fb31167d1f47b5e705480e044d702602 (patch)
treee5474cf88b5eca77515ee8fd72c84d87971b6494
parent8c1784e8c1eac65f66b4a1ecc4b8b0ddd5de9327 (diff)
downloadfactory-boy-54971381fb31167d1f47b5e705480e044d702602.tar
factory-boy-54971381fb31167d1f47b5e705480e044d702602.tar.gz
Add factory.fuzzy (Closes #41).
-rw-r--r--README1
-rw-r--r--docs/changelog.rst3
-rw-r--r--docs/fuzzy.rst88
-rw-r--r--factory/fuzzy.py86
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/test_fuzzy.py104
6 files changed, 281 insertions, 2 deletions
diff --git a/README b/README
index 3756109..cc26087 100644
--- a/README
+++ b/README
@@ -221,6 +221,7 @@ Contents, indices and tables
reference
orms
recipes
+ fuzzy
examples
internals
changelog
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4b24797..100952c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,8 +4,6 @@ ChangeLog
2.0.0 (current)
---------------
-.. note:: This section lists features planned for v2 of factory_boy. Changes announced here may not have been committed to the repository.
-
*New:*
- Allow overriding the base factory class for :func:`~factory.make_factory` and friends.
@@ -15,6 +13,7 @@ ChangeLog
the associated class' constructor
- Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`,
through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`.
+ - Add support for :mod:`~factory.fuzzy` attribute definitions.
*Removed:*
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
new file mode 100644
index 0000000..f1f4085
--- /dev/null
+++ b/docs/fuzzy.rst
@@ -0,0 +1,88 @@
+Fuzzy attributes
+================
+
+.. module:: factory.fuzzy
+
+Some tests may be interested in testing with fuzzy, random values.
+
+This is handled by the :mod:`factory.fuzzy` module, which provides a few
+random declarations.
+
+
+FuzzyAttribute
+--------------
+
+
+.. class:: FuzzyAttribute
+
+ The :class:`FuzzyAttribute` uses an arbitrary callable as fuzzer.
+ It is expected that successive calls of that function return various
+ values.
+
+ .. attribute:: fuzzer
+
+ The callable that generates random values
+
+
+FuzzyChoice
+-----------
+
+
+.. class:: FuzzyChoice(choices)
+
+ The :class:`FuzzyChoice` fuzzer yields random choices from the given
+ iterable.
+
+ .. note:: The passed in :attr:`choices` will be converted into a list at
+ declaration time.
+
+ .. attribute:: choices
+
+ The list of choices to select randomly
+
+
+FuzzyInteger
+------------
+
+.. class:: FuzzyInteger(low[, high])
+
+ The :class:`FuzzyInteger` fuzzer generates random integers within a given
+ inclusive range.
+
+ The :attr:`low` bound may be omitted, in which case it defaults to 0:
+
+ .. code-block:: pycon
+
+ >>> FuzzyInteger(0, 42)
+ >>> fi.low, fi.high
+ 0, 42
+
+ >>> fi = FuzzyInteger(42)
+ >>> fi.low, fi.high
+ 0, 42
+
+ .. attribute:: low
+
+ int, the inclusive lower bound of generated integers
+
+ .. attribute:: high
+
+ int, the inclusive higher bound of generated integers
+
+
+Custom fuzzy fields
+-------------------
+
+Alternate fuzzy fields may be defined.
+They should inherit from the :class:`BaseFuzzyAttribute` class, and override its
+:meth:`~BaseFuzzyAttribute.fuzz` method.
+
+
+.. class:: BaseFuzzyAttribute
+
+ Base class for all fuzzy attributes.
+
+ .. method:: fuzz(self)
+
+ The method responsible for generating random values.
+ *Must* be overridden in subclasses.
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
new file mode 100644
index 0000000..186b4a7
--- /dev/null
+++ b/factory/fuzzy.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+"""Additional declarations for "fuzzy" attribute definitions."""
+
+
+import random
+
+from . import declarations
+
+
+class BaseFuzzyAttribute(declarations.OrderedDeclaration):
+ """Base class for fuzzy attributes.
+
+ Custom fuzzers should override the `fuzz()` method.
+ """
+
+ def fuzz(self):
+ raise NotImplementedError()
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ return self.fuzz()
+
+
+class FuzzyAttribute(BaseFuzzyAttribute):
+ """Similar to LazyAttribute, but yields random values.
+
+ Attributes:
+ function (callable): function taking no parameters and returning a
+ random value.
+ """
+
+ def __init__(self, fuzzer, **kwargs):
+ super(FuzzyAttribute, self).__init__(**kwargs)
+ self.fuzzer = fuzzer
+
+ def fuzz(self):
+ return self.fuzzer()
+
+
+class FuzzyChoice(BaseFuzzyAttribute):
+ """Handles fuzzy choice of an attribute."""
+
+ def __init__(self, choices, **kwargs):
+ self.choices = list(choices)
+ super(FuzzyChoice, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.choice(self.choices)
+
+
+class FuzzyInteger(BaseFuzzyAttribute):
+ """Random integer within a given range."""
+
+ def __init__(self, low, high=None, **kwargs):
+ if high is None:
+ high = low
+ low = 0
+
+ self.low = low
+ self.high = high
+
+ super(FuzzyInteger, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.randint(self.low, self.high)
diff --git a/tests/__init__.py b/tests/__init__.py
index 7531edd..3c620d6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,5 +4,6 @@
from .test_base import *
from .test_containers import *
from .test_declarations import *
+from .test_fuzzy import *
from .test_using import *
from .test_utils import *
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
new file mode 100644
index 0000000..70a2095
--- /dev/null
+++ b/tests/test_fuzzy.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+from factory import fuzzy
+
+from .compat import mock, unittest
+
+
+class FuzzyAttributeTestCase(unittest.TestCase):
+ def test_simple_call(self):
+ d = fuzzy.FuzzyAttribute(lambda: 10)
+
+ res = d.evaluate(2, None, False)
+ self.assertEqual(10, res)
+
+ res = d.evaluate(2, None, False)
+ self.assertEqual(10, res)
+
+
+class FuzzyChoiceTestCase(unittest.TestCase):
+ def test_unbiased(self):
+ options = [1, 2, 3]
+ d = fuzzy.FuzzyChoice(options)
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, options)
+
+ def test_mock(self):
+ options = [1, 2, 3]
+ fake_choice = lambda d: sum(d)
+
+ d = fuzzy.FuzzyChoice(options)
+
+ with mock.patch('random.choice', fake_choice):
+ res = d.evaluate(2, None, False)
+
+ self.assertEqual(6, res)
+
+ def test_generator(self):
+ def options():
+ for i in range(3):
+ yield i
+
+ d = fuzzy.FuzzyChoice(options())
+
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2])
+
+ # And repeat
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2])
+
+
+class FuzzyIntegerTestCase(unittest.TestCase):
+ def test_definition(self):
+ """Tests all ways of defining a FuzzyInteger."""
+ fuzz = fuzzy.FuzzyInteger(2, 3)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertIn(res, [2, 3])
+
+ fuzz = fuzzy.FuzzyInteger(4)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2, 3, 4])
+
+ def test_biased(self):
+ fake_randint = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyInteger(2, 8)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(10, res)
+
+ def test_biased_high_only(self):
+ fake_randint = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyInteger(8)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(8, res)