diff options
-rw-r--r-- | README | 1 | ||||
-rw-r--r-- | docs/changelog.rst | 3 | ||||
-rw-r--r-- | docs/fuzzy.rst | 88 | ||||
-rw-r--r-- | factory/fuzzy.py | 86 | ||||
-rw-r--r-- | tests/__init__.py | 1 | ||||
-rw-r--r-- | tests/test_fuzzy.py | 104 |
6 files changed, 281 insertions, 2 deletions
@@ -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) |