aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2015-02-18 22:00:01 +0100
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2015-02-18 22:00:01 +0100
commit97a88905b7f0f513bd480fe630e43798aba22c74 (patch)
tree5b5bb8307e4b69a2826e5988ecede340a8d9b88b
parentd95bc982cd8480aa44e5282ab1284a9278049066 (diff)
downloadfactory-boy-97a88905b7f0f513bd480fe630e43798aba22c74.tar
factory-boy-97a88905b7f0f513bd480fe630e43798aba22c74.tar.gz
Enable resetting factory.fuzzy's random generator (Closes #175, #185).
Users may now call ``factory.fuzzy.get_random_state()`` to retrieve the current random generator's state (isolated from the one used in Python's ``random``). That state can then be reinjected with ``factory.fuzzy.set_random_state(state)``.
-rw-r--r--docs/changelog.rst4
-rw-r--r--docs/fuzzy.rst30
-rw-r--r--factory/fuzzy.py35
-rw-r--r--tests/test_fuzzy.py50
4 files changed, 97 insertions, 22 deletions
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 018ec60..ebe9930 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -7,6 +7,10 @@ ChangeLog
2.5.0 (master)
--------------
+*New:*
+
+ - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`).
+
*Deprecation:*
- Remove deprecated features from :ref:`v2.4.0`
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index 1480419..0658652 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -338,3 +338,33 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its
The method responsible for generating random values.
*Must* be overridden in subclasses.
+
+
+Managing randomness
+-------------------
+
+Using :mod:`random` in factories allows to "fuzz" a program efficiently.
+However, it's sometimes required to *reproduce* a failing test.
+
+:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`,
+and provides a few helpers for this:
+
+.. method:: get_random_state()
+
+ Call :meth:`get_random_state` to retrieve the random generator's current
+ state.
+
+.. method:: set_random_state(state)
+
+ Use :meth:`set_random_state` to set a custom state into the random generator
+ (fetched from :meth:`get_random_state` in a previous run, for instance)
+
+.. method:: reseed_random(seed)
+
+ The :meth:`reseed_random` function allows to load a chosen seed into the random generator.
+
+
+Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD**
+use :obj:`factory.fuzzy._random` foras a randomness source; this ensures that
+data they generate can be regenerated using the simple state from
+:meth:`get_random_state`.
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 94599b7..0137ba9 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -34,6 +34,25 @@ from . import compat
from . import declarations
+_random = random.Random()
+
+
+def get_random_state():
+ """Retrieve the state of factory.fuzzy's random generator."""
+ return _random.getstate()
+
+
+def set_random_state(state):
+ """Force-set the state of factory.fuzzy's random generator."""
+ return _random.setstate(state)
+
+
+def reseed_random(seed):
+ """Reseed factory.fuzzy's random generator."""
+ r = random.Random(seed)
+ set_random_state(r.getstate())
+
+
class BaseFuzzyAttribute(declarations.OrderedDeclaration):
"""Base class for fuzzy attributes.
@@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute):
"""
def __init__(self, prefix='', length=12, suffix='',
- chars=string.ascii_letters, **kwargs):
+ chars=string.ascii_letters, **kwargs):
super(FuzzyText, self).__init__(**kwargs)
self.prefix = prefix
self.suffix = suffix
@@ -89,7 +108,7 @@ class FuzzyText(BaseFuzzyAttribute):
self.chars = tuple(chars) # Unroll iterators
def fuzz(self):
- chars = [random.choice(self.chars) for _i in range(self.length)]
+ chars = [_random.choice(self.chars) for _i in range(self.length)]
return self.prefix + ''.join(chars) + self.suffix
@@ -101,7 +120,7 @@ class FuzzyChoice(BaseFuzzyAttribute):
super(FuzzyChoice, self).__init__(**kwargs)
def fuzz(self):
- return random.choice(self.choices)
+ return _random.choice(self.choices)
class FuzzyInteger(BaseFuzzyAttribute):
@@ -119,7 +138,7 @@ class FuzzyInteger(BaseFuzzyAttribute):
super(FuzzyInteger, self).__init__(**kwargs)
def fuzz(self):
- return random.randrange(self.low, self.high + 1, self.step)
+ return _random.randrange(self.low, self.high + 1, self.step)
class FuzzyDecimal(BaseFuzzyAttribute):
@@ -137,7 +156,7 @@ class FuzzyDecimal(BaseFuzzyAttribute):
super(FuzzyDecimal, self).__init__(**kwargs)
def fuzz(self):
- base = compat.float_to_decimal(random.uniform(self.low, self.high))
+ base = compat.float_to_decimal(_random.uniform(self.low, self.high))
return base.quantize(decimal.Decimal(10) ** -self.precision)
@@ -155,7 +174,7 @@ class FuzzyFloat(BaseFuzzyAttribute):
super(FuzzyFloat, self).__init__(**kwargs)
def fuzz(self):
- return random.uniform(self.low, self.high)
+ return _random.uniform(self.low, self.high)
class FuzzyDate(BaseFuzzyAttribute):
@@ -175,7 +194,7 @@ class FuzzyDate(BaseFuzzyAttribute):
self.end_date = end_date.toordinal()
def fuzz(self):
- return datetime.date.fromordinal(random.randint(self.start_date, self.end_date))
+ return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date))
class BaseFuzzyDateTime(BaseFuzzyAttribute):
@@ -215,7 +234,7 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute):
delta = self.end_dt - self.start_dt
microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
- offset = random.randint(0, microseconds)
+ offset = _random.randint(0, microseconds)
result = self.start_dt + datetime.timedelta(microseconds=offset)
if self.force_year is not None:
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index 1caeb0a..fd32705 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -55,7 +55,7 @@ class FuzzyChoiceTestCase(unittest.TestCase):
d = fuzzy.FuzzyChoice(options)
- with mock.patch('random.choice', fake_choice):
+ with mock.patch('factory.fuzzy._random.choice', fake_choice):
res = d.evaluate(2, None, False)
self.assertEqual(6, res)
@@ -93,7 +93,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(2, 8)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((2 + 8 + 1) * 1, res)
@@ -103,7 +103,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(8)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((0 + 8 + 1) * 1, res)
@@ -113,7 +113,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(5, 8, 3)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((5 + 8 + 1) * 3, res)
@@ -146,7 +146,7 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(2.0, 8.0)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('10.0'), res)
@@ -156,7 +156,7 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(8.0)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('8.0'), res)
@@ -166,7 +166,7 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(8.0, precision=3)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res)
@@ -214,7 +214,7 @@ class FuzzyDateTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.date(2013, 1, 16), res)
@@ -225,7 +225,7 @@ class FuzzyDateTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDate(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.date(2013, 1, 2), res)
@@ -332,7 +332,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 16), res)
@@ -343,7 +343,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 2), res)
@@ -450,7 +450,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res)
@@ -461,7 +461,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDateTime(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res)
@@ -486,7 +486,7 @@ class FuzzyTextTestCase(unittest.TestCase):
chars = ['a', 'b', 'c']
fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4)
- with mock.patch('random.choice', fake_choice):
+ with mock.patch('factory.fuzzy._random.choice', fake_choice):
res = fuzz.evaluate(2, None, False)
self.assertEqual('preaaaapost', res)
@@ -504,3 +504,25 @@ class FuzzyTextTestCase(unittest.TestCase):
for char in res:
self.assertIn(char, ['a', 'b', 'c'])
+
+
+class FuzzyRandomTestCase(unittest.TestCase):
+ def test_seeding(self):
+ fuzz = fuzzy.FuzzyInteger(1, 1000)
+
+ fuzzy.reseed_random(42)
+ value = fuzz.evaluate(sequence=1, obj=None, create=False)
+
+ fuzzy.reseed_random(42)
+ value2 = fuzz.evaluate(sequence=1, obj=None, create=False)
+ self.assertEqual(value, value2)
+
+ def test_reset_state(self):
+ fuzz = fuzzy.FuzzyInteger(1, 1000)
+
+ state = fuzzy.get_random_state()
+ value = fuzz.evaluate(sequence=1, obj=None, create=False)
+
+ fuzzy.set_random_state(state)
+ value2 = fuzz.evaluate(sequence=1, obj=None, create=False)
+ self.assertEqual(value, value2)