pax_global_header00006660000000000000000000000064130016351020014501gustar00rootroot0000000000000052 comment=b9a52c63f8de49a0a1f62d13e463c73d2693f6b9 nose-random-1.0.0/000077500000000000000000000000001300163510200137215ustar00rootroot00000000000000nose-random-1.0.0/.gitignore000066400000000000000000000012761300163510200157170ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ nose-random-1.0.0/LICENSE000066400000000000000000000021001300163510200147170ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Zoomer Analytics LLC 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. nose-random-1.0.0/README.md000066400000000000000000000054261300163510200152070ustar00rootroot00000000000000# nose-random `nose-random` is designed to facilitate [Monte-Carlo style](https://en.wikipedia.org/wiki/Monte_Carlo_method) unit testing. The idea is to improve testing by running your code against a large number of randomly generated input scenarios. Even with random testing it's important that test success/failure is reproducible, otherwise it's hard to * know if you've fixed a failing test * know if an test fails only on some machines or configurations and not others * debug a failing test `nose-random` avoids this pitfall because it * uses a fixed seed so that each test run is identical * tells you which scenario caused a test to fail * lets you to run the test only on a specific scenario to facilitate debugging ## Installation pip install git+https://github.com/ZoomerAnalytics/nose-random.git or clone the repo and run `python setup.py install`. ## Usage The following [example](examples/tests.py) shows how to set up a randomized test. ```python # tests.py import unittest from nose_random import randomize class RandomTestCase(unittest.TestCase): def generate_scenario(self, rng): return rng.random(), 10 * rng.random() @randomize(1000, generate_scenario) def failing_test(self, scenario): x, y = scenario self.assertLess(x, y) @randomize(1000, generate_scenario) def passing_test(self, scenario): x, y = scenario self.assertLess(x, y + 1) ``` Running `nosetests` from the same folder will produce the following output: F. ====================================================================== FAIL: failing_test (tests.RandomTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "c:\dev\nose-random\nose_random\__init__.py", line 62, in randomized_test test(self, scenario) File "C:\Dev\nose-random\examples\tests.py", line 13, in failing_test self.assertLess(x, y) AssertionError: 0.9903329105632304 not less than 0.0015605977653532221 with scenario 2FDO1B08J20A (27 of 1000) ---------------------------------------------------------------------- Ran 2 tests in 0.067s This means that the `failing_test` function succeeded with the first 26 scenarios but then failed with the 27th. To debug this scenario you can run the same command specifying the scenario id. nosetests --scenario=2FDO1B08J20A This will cause the tests to only be run against the failing scenario (note '1 of 1' as opposed to '27 of 1000'). AssertionError: 0.9903329105632304 not less than 0.0015605977653532221 with scenario 2FDO1B08J20A (1 of 1) # PyCharm Note that the module will also pick up the `--scenario=` string if passed as a parameter to a PyCharm unittest run/debug configuration. nose-random-1.0.0/examples/000077500000000000000000000000001300163510200155375ustar00rootroot00000000000000nose-random-1.0.0/examples/tests.py000066400000000000000000000007141300163510200172550ustar00rootroot00000000000000import unittest from nose_random import randomize class RandomTestCase(unittest.TestCase): def generate_scenario(self, rng): return rng.random(), 10 * rng.random() @randomize(1000, generate_scenario) def failing_test(self, scenario): x, y = scenario self.assertLess(x, y) @randomize(1000, generate_scenario) def passing_test(self, scenario): x, y = scenario self.assertLess(x, y + 1)nose-random-1.0.0/nose_random/000077500000000000000000000000001300163510200162255ustar00rootroot00000000000000nose-random-1.0.0/nose_random/__init__.py000066400000000000000000000043211300163510200203360ustar00rootroot00000000000000__version__ = '0.0.1' from nose.plugins import Plugin import os import sys PY3 = (sys.version_info[0] == 3) from random import Random _missing = object() class NoseRandomConfig(object): def __init__(self): self.is_nose_plugin = False self._scenario = _missing @property def scenario(self): if self._scenario is _missing: import sys for arg in sys.argv: if arg.startswith('--scenario='): self._scenario = arg[len('--scenario='):] return self._scenario @scenario.setter def scenario(self, value): self._scenario = value config = NoseRandomConfig() class NoseRandomPlugin(Plugin): def options(self, parser, env=os.environ): parser.add_option('--scenario', type='str', dest='scenario', help="Specify the scenario seed for debugging random tests.") def configure(self, options, conf): config.scenario = getattr(options, 'scenario', None) def _generate_tag(n, rng): return ''.join(rng.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(n)) def randomize(n, scenario_generator, seed=12038728732): def decorator(test): def randomized_test(self): if config.scenario is not None: nseeds = 1 seeds = [config.scenario] else: rng_seed = Random(seed) nseeds = n seeds = (_generate_tag(12, rng_seed) for i in range(n)) # (rng_seed.getrandbits(32) for i in range(n)) for i, rseed in enumerate(seeds): rng = Random(rseed) scenario = scenario_generator(self, rng) try: test(self, scenario) except Exception as e: import sys if PY3: raise type(e).with_traceback(type(e)('%s with scenario %s (%i of %i)' % (e.message, rseed, i+1, nseeds)), sys.exc_info()[2]) else: raise (type(e), type(e)('%s with scenario %s (%i of %i)' % (e.message, rseed, i+1, nseeds)), sys.exc_info()[2]) return randomized_test return decoratornose-random-1.0.0/setup.py000066400000000000000000000016151300163510200154360ustar00rootroot00000000000000import os import re from setuptools import setup, find_packages with open(os.path.join(os.path.dirname(__file__), 'nose_random', '__init__.py')) as f: version = re.compile(r".*__version__ = '(.*?)'", re.S).match(f.read()).group(1) setup( name='nose-random', packages=find_packages(), version=version, description='Random scenario testing in Nose', author='Zoomer Analytics LLC', author_email='eric.reynolds@zoomeranalytics.com', url='https://github.com/ZoomerAnalytics/nose-random', keywords=['nose', 'tests', 'nosetests', 'test', 'unit', 'testing', 'random', 'stochastic', 'entropy', 'randomized', 'scenario'], entry_points={ 'nose.plugins.0.10': [ 'nose_random = nose_random:NoseRandomPlugin' ] }, classifiers=[ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ], )