genty-1.3.0/0000755€BìÙ΀HÖ¢0000000000012617211551014117 5ustar jmeadows00000000000000genty-1.3.0/genty/0000755€BìÙ΀HÖ¢0000000000012617211551015245 5ustar jmeadows00000000000000genty-1.3.0/genty/__init__.py0000644€BìÙ΀HÖ¢0000000036312616747501017370 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals from .genty import genty from .genty_dataset import genty_dataset from .genty_dataset import genty_dataprovider from .genty_repeat import genty_repeat from .genty_args import genty_args genty-1.3.0/genty/genty.py0000644€BìÙ΀HÖ¢0000003617512616747501016771 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals import functools from itertools import chain import math import types import re import six import sys from .genty_args import GentyArgs from .private import encode_non_ascii_string REPLACE_FOR_PERIOD_CHAR = '\xb7' def genty(target_cls): """ This decorator takes the information provided by @genty_dataset, @genty_dataprovider, and @genty_repeat and generates the corresponding test methods. :param target_cls: Test class whose test methods have been decorated. :type target_cls: `class` """ tests = _expand_tests(target_cls) tests_with_datasets = _expand_datasets(tests) tests_with_datasets_and_repeats = _expand_repeats(tests_with_datasets) _add_new_test_methods(target_cls, tests_with_datasets_and_repeats) return target_cls def _expand_tests(target_cls): """ Generator of all the test unbound functions in the given class. :param target_cls: Target test class. :type target_cls: `class` :return: Generator of all the test_methods in the given class yielding tuples of method name and unbound function. :rtype: `generator` of `tuple` of (`unicode`, `function`) """ entries = dict(six.iteritems(target_cls.__dict__)) for key, value in six.iteritems(entries): if key.startswith('test') and isinstance(value, types.FunctionType): if not hasattr(value, 'genty_generated_test'): yield key, value def _expand_datasets(test_functions): """ Generator producing test_methods, with an optional dataset. :param test_functions: Iterator over tuples of test name and test unbound function. :type test_functions: `iterator` of `tuple` of (`unicode`, `function`) :return: Generator yielding a tuple of - method_name : Name of the test method - unbound function : Unbound function that will be the test method. - dataset name : String representation of the given dataset - dataset : Tuple representing the args for a test - param factory : Function that returns params for the test method :rtype: `generator` of `tuple` of ( `unicode`, `function`, `unicode` or None, `tuple` or None, `function` or None, ) """ for name, func in test_functions: dataset_tuples = chain( [(None, getattr(func, 'genty_datasets', {}))], getattr(func, 'genty_dataproviders', []), ) no_datasets = True for dataprovider, datasets in dataset_tuples: for dataset_name, dataset in six.iteritems(datasets): no_datasets = False yield name, func, dataset_name, dataset, dataprovider if no_datasets: # yield the original test method, unaltered yield name, func, None, None, None def _expand_repeats(test_functions): """ Generator producing test_methods, with any repeat count unrolled. :param test_functions: Sequence of tuples of - method_name : Name of the test method - unbound function : Unbound function that will be the test method. - dataset name : String representation of the given dataset - dataset : Tuple representing the args for a test - param factory : Function that returns params for the test method :type test_functions: `iterator` of `tuple` of (`unicode`, `function`, `unicode` or None, `tuple` or None, `function`) :return: Generator yielding a tuple of (method_name, unbound function, dataset, name dataset, repeat_suffix) :rtype: `generator` of `tuple` of (`unicode`, `function`, `unicode` or None, `tuple` or None, `function`, `unicode`) """ for name, func, dataset_name, dataset, dataprovider in test_functions: repeat_count = getattr(func, 'genty_repeat_count', 0) if repeat_count: for i in range(1, repeat_count + 1): repeat_suffix = _build_repeat_suffix(i, repeat_count) yield ( name, func, dataset_name, dataset, dataprovider, repeat_suffix, ) else: yield name, func, dataset_name, dataset, dataprovider, None def _add_new_test_methods(target_cls, tests_with_datasets_and_repeats): """Define the given tests in the given class. :param target_cls: Test class where to define the given test methods. :type target_cls: `class` :param tests_with_datasets_and_repeats: Sequence of tuples describing the new test to add to the class. (method_name, unbound function, dataset name, dataset, dataprovider, repeat_suffix) :type tests_with_datasets_and_repeats: Sequence of `tuple` of (`unicode`, `function`, `unicode` or None, `tuple` or None, `function`, `unicode`) """ for test_info in tests_with_datasets_and_repeats: ( method_name, func, dataset_name, dataset, dataprovider, repeat_suffix, ) = test_info # Remove the original test_method as it's superseded by this # generated method. is_first_reference = _delete_original_test_method( target_cls, method_name, ) # However, if that test_method is referenced by name in sys.argv # Then take 1 of the generated methods (we take the first) and # give that generated method the original name... so that the reference # can find an actual test method. if is_first_reference and _is_referenced_in_argv(method_name): dataset_name = None repeat_suffix = None _add_method_to_class( target_cls, method_name, func, dataset_name, dataset, dataprovider, repeat_suffix, ) def _is_referenced_in_argv(method_name): """ Various test runners allow one to run a specific test like so: python -m unittest -v . Return True is the given method name is so referenced. :param method_name: Base name of the method to add. :type method_name: `unicode` :return: Is the given method referenced by the command line. :rtype: `bool` """ expr = '.*[:.]{0}$'.format(method_name) regex = re.compile(expr) return any(regex.match(arg) for arg in sys.argv) def _build_repeat_suffix(iteration, count): """ Return the suffix string to identify iteration X out of Y. For example, with a count of 100, this will build strings like "iteration_053" or "iteration_008". :param iteration: Current iteration. :type iteration: `int` :param count: Total number of iterations. :type count: `int` :return: Repeat suffix. :rtype: `unicode` """ format_width = int(math.ceil(math.log(count + 1, 10))) new_suffix = 'iteration_{0:0{width}d}'.format( iteration, width=format_width ) return new_suffix def _delete_original_test_method(target_cls, name): """ Delete an original test method with the given name. :param target_cls: Target class. :type target_cls: `class` :param name: Name of the method to remove. :type name: `unicode` :return: True if the original method existed :rtype: `bool` """ attribute = getattr(target_cls, name, None) if attribute and not getattr(attribute, 'genty_generated_test', None): try: delattr(target_cls, name) except AttributeError: pass return True else: return False def _build_final_method_name( method_name, dataset_name, dataprovider_name, repeat_suffix, ): """ Return a nice human friendly name, that almost looks like code. Example: a test called 'test_something' with a dataset of (5, 'hello') Return: "test_something(5, 'hello')" Example: a test called 'test_other_stuff' with dataset of (9) and repeats Return: "test_other_stuff(9) iteration_" :param method_name: Base name of the method to add. :type method_name: `unicode` :param dataset_name: Base name of the data set. :type dataset_name: `unicode` or None :param dataprovider_name: If there's a dataprovider involved, then this is its name. :type dataprovider_name: `unicode` or None :param repeat_suffix: Suffix to append to the name of the generated method. :type repeat_suffix: `unicode` or None :return: The fully composed name of the generated test method. :rtype: `unicode` """ # For tests using a dataprovider, append "_" to # the test method name suffix = '' if dataprovider_name: suffix = '_{0}'.format(dataprovider_name) if not dataset_name and not repeat_suffix: return '{0}{1}'.format(method_name, suffix) if dataset_name: # Nosetest multi-processing code parses the full test name # to discern package/module names. Thus any periods in the test-name # causes that code to fail. So replace any periods with the unicode # middle-dot character. Yes, this change is applied independent # of the test runner being used... and that's fine since there is # no real contract as to how the fabricated tests are named. dataset_name = dataset_name.replace('.', REPLACE_FOR_PERIOD_CHAR) # Place data_set info inside parens, as if it were a function call suffix = '{0}({1})'.format(suffix, dataset_name or "") if repeat_suffix: suffix = '{0} {1}'.format(suffix, repeat_suffix) test_method_name_for_dataset = "{0}{1}".format( method_name, suffix, ) return test_method_name_for_dataset def _build_dataset_method(method, dataset): """ Return a fabricated method that marshals the dataset into parameters for given 'method' :param method: The underlying test method. :type method: `callable` :param dataset: Tuple or GentyArgs instance containing the args of the dataset. :type dataset: `tuple` or :class:`GentyArgs` :return: Return an unbound function that will become a test method :rtype: `function` """ if isinstance(dataset, GentyArgs): test_method = lambda my_self: method( my_self, *dataset.args, **dataset.kwargs ) else: test_method = lambda my_self: method( my_self, *dataset ) return test_method def _build_dataprovider_method(method, dataset, dataprovider): """ Return a fabricated method that calls the dataprovider with the given dataset, and marshals the return value from that into params to the underlying test 'method'. :param method: The underlying test method. :type method: `callable` :param dataset: Tuple or GentyArgs instance containing the args of the dataset. :type dataset: `tuple` or :class:`GentyArgs` :param dataprovider: The unbound function that's responsible for generating the actual params that will be passed to the test function. :type dataprovider: `callable` :return: Return an unbound function that will become a test method :rtype: `function` """ if isinstance(dataset, GentyArgs): final_args = dataset.args final_kwargs = dataset.kwargs else: final_args = dataset final_kwargs = {} def test_method_wrapper(my_self): args = dataprovider( my_self, *final_args, **final_kwargs ) kwargs = {} if isinstance(args, GentyArgs): kwargs = args.kwargs args = args.args elif not isinstance(args, (tuple, list)): args = (args, ) return method(my_self, *args, **kwargs) return test_method_wrapper def _build_test_method(method, dataset, dataprovider=None): """ Return a fabricated method that marshals the dataset into parameters for given 'method' :param method: The underlying test method. :type method: `callable` :param dataset: Tuple or GentyArgs instance containing the args of the dataset. :type dataset: `tuple` or :class:`GentyArgs` or None :param dataprovider: The unbound function that's responsible for generating the actual params that will be passed to the test function. Can be None :type dataprovider: `callable` or None :return: Return an unbound function that will become a test method :rtype: `function` """ if dataprovider: test_method = _build_dataprovider_method(method, dataset, dataprovider) elif dataset: test_method = _build_dataset_method(method, dataset) else: test_method = method return test_method def _add_method_to_class( target_cls, method_name, func, dataset_name, dataset, dataprovider, repeat_suffix, ): """ Add the described method to the given class. :param target_cls: Test class to which to add a method. :type target_cls: `class` :param method_name: Base name of the method to add. :type method_name: `unicode` :param func: The underlying test function to call. :type func: `callable` :param dataset_name: Base name of the data set. :type dataset_name: `unicode` or None :param dataset: Tuple containing the args of the dataset. :type dataset: `tuple` or None :param repeat_suffix: Suffix to append to the name of the generated method. :type repeat_suffix: `unicode` or None :param dataprovider: The unbound function that's responsible for generating the actual params that will be passed to the test function. Can be None. :type dataprovider: `callable` """ # pylint: disable=too-many-arguments test_method_name_for_dataset = _build_final_method_name( method_name, dataset_name, dataprovider.__name__ if dataprovider else None, repeat_suffix, ) test_method_for_dataset = _build_test_method(func, dataset, dataprovider) test_method_for_dataset = functools.update_wrapper( test_method_for_dataset, func, ) test_method_name_for_dataset = encode_non_ascii_string( test_method_name_for_dataset, ) test_method_for_dataset.__name__ = test_method_name_for_dataset test_method_for_dataset.genty_generated_test = True # Add the method to the class under the proper name setattr(target_cls, test_method_name_for_dataset, test_method_for_dataset) genty-1.3.0/genty/genty_args.py0000644€BìÙ΀HÖ¢0000000477612616747501020007 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals from itertools import chain import six from .private import format_arg, format_kwarg class GentyArgs(object): """ Store args and kwargs for use in a genty-generated test. """ def __init__(self, *args, **kwargs): super(GentyArgs, self).__init__() self._args = args self._kwargs = kwargs @property def args(self): """Return tuple of positional arguments to be passed to the test.""" return self._args @property def kwargs(self): """Return dictionary of keyword arguments to be passed to the test.""" return self._kwargs def __iter__(self): """Allow iterating over the argument list. First, yield value of args in given order. Then yield kwargs in sorted order, formatted as key_equals_value. """ sorted_kwargs = sorted(six.iteritems(self._kwargs)) return chain( (format_arg(arg) for arg in self._args), (format_kwarg(k, v) for k, v in sorted_kwargs), ) def genty_args(*args, **kwargs): """ Used to pass args and kwargs to a test wrapped with @genty_dataset. Runs the test with the same arguments and keyword arguments passed to genty_args. genty_args is usefule for tests with a large number of arguments or optional arguments. To use, simply pass your arguments and keyword arguments to genty_args in the same way you'd like them to be passed to your test: @genty_dataset( genty_args('a1', 'b1', 1, 'd1'), genty_args('a2', 'b2', d='d2') ) def test_function(a, b, c=0, d=None) ... For each genty_args call, a suffix identifying the arguments will be built by concatenating the positional argument values, and then concatenating the keyword argument names and values (formatted like parameter_equals_value). For example: @genty_dataset( genty_args('a1', 'b1', 1, 'd1'), genty_args('a2', 'b2', d='d2') ) def test_function(a, b, c=0, d=None) ... produces tests named test_function('a1', 'b1', 1, 'd1') and test_function('a2', 'b2', d='d2') :param args: Ordered arguments that should be sent to the test. :type args: `tuple` of varies :param kwargs: Keyword arguments that should be sent to the test. :type kwargs: `dict` of `unicode` to varies """ return GentyArgs(*args, **kwargs) genty-1.3.0/genty/genty_dataset.py0000644€BìÙ΀HÖ¢0000001453412616747501020471 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals try: from collections import OrderedDict except ImportError: # pylint:disable=import-error from ordereddict import OrderedDict # pylint:enable=import-error import six from .genty_args import GentyArgs from .private import format_arg def genty_dataprovider(builder_function): """Decorator defining that this test gets parameters from the given build_function. :param builder_function: A callable that returns parameters that will be passed to the method decorated by this decorator. If the builder_function returns a tuple or list, then that will be passed as *args to the decorated method. If the builder_function returns a :class:`GentyArgs`, then that will be used to pass *args and **kwargs to the decorated method. Any other return value will be treated as a single parameter, and passed as such to the decorated method. :type builder_function: `callable` """ datasets = getattr(builder_function, 'genty_datasets', {None: ()}) def wrap(test_method): # Save the data providers in the test method. This data will be # consumed by the @genty decorator. if not hasattr(test_method, 'genty_dataproviders'): test_method.genty_dataproviders = [] test_method.genty_dataproviders.append( (builder_function, datasets), ) return test_method return wrap def genty_dataset(*args, **kwargs): """Decorator defining data sets to provide to a test. Inspired by http://sebastian-bergmann.de/archives/ 702-Data-Providers-in-PHPUnit-3.2.html The canonical way to call @genty_dataset, with each argument each representing a data set to be injected in the test method call: @genty_dataset( ('a1', 'b1'), ('a2', 'b2'), ) def test_some_function(a, b) ... If the test function takes only one parameter, you can replace the tuples by a single value. So instead of the more verbose: @genty_dataset( ('c1',), ('c2',), ) def test_some_other_function(c) ... One can write: @genty_dataset('c1', 'c2') def test_some_other_function(c) ... For each set of arguments, a suffix identifying that argument set is built by concatenating the string representation of the arguments together. You can control the test names for each data set by passing the data sets as keyword args, where the keyword is the desired suffix. For example: @genty_dataset( ('a1', 'b1), ) def test_function(a, b) ... produces a test named 'test_function_for_a1_and_b1', while @genty_dataset( happy_path=('a1', 'b1'), ) def test_function(a, b) ... produces a test named test_function_for_happy_path. These are just parameters to a method call, so one can have unnamed args first followed by keyword args @genty_dataset( ('x', 'y'), ('p', 'q'), Monday=('a1', 'b1'), Tuesday=('t1', 't2'), ) def test_function(a, b) ... Finally, datasets can be chained. Useful for example if there are distinct sets of params that make sense (cleaner, more readable, or semantically nicer) if kept separate. A fabricated example: @genty_dataset( *([i for i in range(10)] + [(i, i) for i in range(10)]) ) def test_some_other_function(param1, param2=None) ... -- vs -- @genty_dataset(*[i for i in range(10)]) @genty_dataset(*[(i, i) for i in range(10)]) def test_some_other_function(param1, param2=None) ... If the names of datasets conflict across chained genty_datasets, the key&value pair from the outer (first) decorator will override the data from the inner. :param args: Tuple of unnamed data sets. :type args: `tuple` of varies :param kwargs: Dict of pre-named data sets. :type kwargs: `dict` of `unicode` to varies """ datasets = _build_datasets(*args, **kwargs) def wrap(test_method): # Save the datasets in the test method. This data will be consumed # by the @genty decorator. if not hasattr(test_method, 'genty_datasets'): test_method.genty_datasets = OrderedDict() test_method.genty_datasets.update(datasets) return test_method return wrap def _build_datasets(*args, **kwargs): """Build the datasets into a dict, where the keys are the name of the data set and the values are the data sets themselves. :param args: Tuple of unnamed data sets. :type args: `tuple` of varies :param kwargs: Dict of pre-named data sets. :type kwargs: `dict` of `unicode` to varies :return: The dataset dict. :rtype: `dict` """ datasets = OrderedDict() _add_arg_datasets(datasets, args) _add_kwarg_datasets(datasets, kwargs) return datasets def _add_arg_datasets(datasets, args): """Add data sets of the given args. :param datasets: The dict where to accumulate data sets. :type datasets: `dict` :param args: Tuple of unnamed data sets. :type args: `tuple` of varies """ for dataset in args: # turn a value into a 1-tuple. if not isinstance(dataset, (tuple, GentyArgs)): dataset = (dataset,) # Create a test_name_suffix - basically the parameter list if isinstance(dataset, GentyArgs): dataset_strings = dataset # GentyArgs supports iteration else: dataset_strings = [format_arg(data) for data in dataset] test_method_suffix = ", ".join(dataset_strings) datasets[test_method_suffix] = dataset def _add_kwarg_datasets(datasets, kwargs): """Add data sets of the given kwargs. :param datasets: The dict where to accumulate data sets. :type datasets: `dict` :param kwargs: Dict of pre-named data sets. :type kwargs: `dict` of `unicode` to varies """ for test_method_suffix, dataset in six.iteritems(kwargs): datasets[test_method_suffix] = dataset genty-1.3.0/genty/genty_repeat.py0000644€BìÙ΀HÖ¢0000000163712616747501020324 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals def genty_repeat(count): """ To use in conjunction with a TestClass wrapped with @genty. Runs the wrapped test 'count' times: @genty_repeat(count) def test_some_function(self) ... Can also wrap a test already decorated with @genty_dataset @genty_repeat(3) @genty_dataset(True, False) def test_some__other_function(self, bool_value): ... This will run 6 tests in total, 3 each of the True and False cases. :param count: The number of times to run the test. :type count: `int` """ if count < 0: raise ValueError( "Really? Can't have {0} iterations. Please pick a value >= 0." .format(count) ) def wrap(test_method): test_method.genty_repeat_count = count return test_method return wrap genty-1.3.0/genty/private/0000755€BìÙ΀HÖ¢0000000000012617211551016717 5ustar jmeadows00000000000000genty-1.3.0/genty/private/__init__.py0000644€BìÙ΀HÖ¢0000000213612616747501021042 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals import six def format_kwarg(key, value): """ Return a string of form: "key=" If 'value' is a string, we want it quoted. The goal is to make the string a named parameter in a method call. """ translator = repr if isinstance(value, six.string_types) else six.text_type arg_value = translator(value) return '{0}={1}'.format(key, arg_value) def format_arg(value): """ :param value: Some value in a dataset. :type value: varies :return: unicode representation of that value :rtype: `unicode` """ translator = repr if isinstance(value, six.string_types) else six.text_type return translator(value) def encode_non_ascii_string(string): """ :param string: The string to be encoded :type string: unicode or str :return: The encoded string :rtype: str """ encoded_string = string.encode('utf-8', 'replace') if six.PY3: encoded_string = encoded_string.decode() return encoded_string genty-1.3.0/genty.egg-info/0000755€BìÙ΀HÖ¢0000000000012617211551016737 5ustar jmeadows00000000000000genty-1.3.0/genty.egg-info/dependency_links.txt0000644€BìÙ΀HÖ¢0000000000112617211540023003 0ustar jmeadows00000000000000 genty-1.3.0/genty.egg-info/not-zip-safe0000644€BìÙ΀HÖ¢0000000000112616747667021212 0ustar jmeadows00000000000000 genty-1.3.0/genty.egg-info/PKG-INFO0000644€BìÙ΀HÖ¢0000005065312617211540020043 0ustar jmeadows00000000000000Metadata-Version: 1.1 Name: genty Version: 1.3.0 Summary: Allows you to run a test with multiple data sets Home-page: https://github.com/box/genty Author: Box Author-email: oss@box.com License: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Description: genty ===== .. image:: http://opensource.box.com/badges/active.svg :target: http://opensource.box.com/badges .. image:: https://travis-ci.org/box/genty.png?branch=master :target: https://travis-ci.org/box/genty .. image:: https://img.shields.io/pypi/v/genty.svg :target: https://pypi.python.org/pypi/genty .. image:: https://img.shields.io/pypi/dm/genty.svg :target: https://pypi.python.org/pypi/genty About ----- Genty, pronounced "gen-tee", stands for "generate tests". It promotes generative testing, where a single test can execute over a variety of input. Genty makes this a breeze. For example, consider a file sample.py containing both the code under test and the tests: .. code-block:: python from genty import genty, genty_repeat, genty_dataset from unittest import TestCase # Here's the class under test class MyClass(object): def add_one(self, x): return x + 1 # Here's the test code @genty class MyClassTests(TestCase): @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) Running the ``MyClassTests`` using the default unittest runner .. code-block:: console $ python -m unittest -v sample test_add_one(0, 1) (sample.MyClassTests) ... ok test_add_one(100000, 100001) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Instead of having to write multiple independent tests for various test cases, code can be refactored and parametrized using genty! It produces readable tests. It produces maintainable tests. It produces expressive tests. Another option is running the same test multiple times. This is useful in stress tests or when exercising code looking for race conditions. This particular test .. code-block:: python @genty_repeat(3) def test_adding_one_to_zero(self): self.assertEqual(1, MyClass().add_one(0)) would be run 3 times, producing output like .. code-block:: console $ python -m unittest -v sample test_adding_one() iteration_1 (sample.MyClassTests) ... ok test_adding_one() iteration_2 (sample.MyClassTests) ... ok test_adding_one() iteration_3 (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK The 2 techniques can be combined: .. code-block:: python @genty_repeat(2) @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) There are more options to explore including naming your datasets and ``genty_args``. .. code-block:: python @genty_dataset( default_case=(0, 1), limit_case=(999, 1000), error_case=genty_args(-1, -1, is_something=False), ) def test_complex(self, value1, value2, optional_value=None, is_something=True): ... would run 3 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_complex(default_case) (sample.MyClassTests) ... ok test_complex(limit_case) (sample.MyClassTests) ... ok test_complex(error_case) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.003s OK The ``@genty_datasets`` can be chained together. This is useful, for example, if there are semantically different datasets so keeping them separate would help expressiveness. .. code-block:: python @genty_dataset(10, 100) @genty_dataset('first', 'second') def test_composing(self, parameter_value): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_composing(10) (sample.MyClassTests) ... ok test_composing(100) (sample.MyClassTests) ... ok test_composing(u'first') (sample.MyClassTests) ... ok test_composing(u'second') (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK Sometimes the parameters to a test can't be determined at module load time. For example, some test might be based on results from some http request. And first the test needs to authenticate, etc. This is supported using the ``@genty_dataprovider`` decorator like so: .. code-block:: python def setUp(self): super(MyClassTests, self).setUp() # http authentication happens # And imagine that _some_function is actually some http request self._some_function = lambda x, y: ((x + y), (x - y), (x * y)) @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_heavy_calculate(100, 1) (sample.MyClassTests) ... ok test_heavy_calculate(1000, 100) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Notice here how the name of the helper (``calculate``) is added to the names of the 2 executed test cases. Like ``@genty_dataset``, ``@genty_dataprovider`` can be chained together. Enjoy! Deferred Parameterization ------------------------- Parametrized tests where the final parameters are not determined until test execution time. It looks like so: .. code-block:: python @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated, perhaps in # some Test.setUp() method. # Let's imagine that _some_function requires that authentication. # And it returns a 3-tuple, matching that signature of # of the test(s) decorated with this 'calculate' method. return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... The ``calculate()`` method is called 2 times based on the ``@genty_dataset`` decorator, and each of it's return values define the final parameters that will be given to the method ``test_heavy(...)``. Installation ------------ To install, simply: .. code-block:: console pip install genty Contributing ------------ See `CONTRIBUTING.rst `_. Setup ~~~~~ Create a virtual environment and install packages - .. code-block:: console mkvirtualenv genty pip install -r requirements-dev.txt Testing ~~~~~~~ Run all tests using - .. code-block:: console tox The tox tests include code style checks via pep8 and pylint. The tox tests are configured to run on Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy 2.6. Copyright and License --------------------- :: Copyright 2015 Box, Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Keywords: genty,tests,generative,unittest Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Software Development Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X genty-1.3.0/genty.egg-info/requires.txt0000644€BìÙ΀HÖ¢0000000000312617211540021326 0ustar jmeadows00000000000000sixgenty-1.3.0/genty.egg-info/SOURCES.txt0000644€BìÙ΀HÖ¢0000000073312617211541020625 0ustar jmeadows00000000000000LICENSE MANIFEST.in README.rst setup.py genty/__init__.py genty/genty.py genty/genty_args.py genty/genty_dataset.py genty/genty_repeat.py genty.egg-info/PKG-INFO genty.egg-info/SOURCES.txt genty.egg-info/dependency_links.txt genty.egg-info/not-zip-safe genty.egg-info/requires.txt genty.egg-info/top_level.txt genty/private/__init__.py test/test_case_base.py test/test_example.py test/test_genty.py test/test_genty_args.py test/test_genty_dataset.py test/test_genty_repeat.pygenty-1.3.0/genty.egg-info/top_level.txt0000644€BìÙ΀HÖ¢0000000000612617211540021463 0ustar jmeadows00000000000000genty genty-1.3.0/LICENSE0000644€BìÙ΀HÖ¢0000002254612616747501015145 0ustar jmeadows00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS genty-1.3.0/MANIFEST.in0000644€BìÙ΀HÖ¢0000000003312616747501015661 0ustar jmeadows00000000000000include README.rst LICENSE genty-1.3.0/PKG-INFO0000644€BìÙ΀HÖ¢0000005065312617211551015225 0ustar jmeadows00000000000000Metadata-Version: 1.1 Name: genty Version: 1.3.0 Summary: Allows you to run a test with multiple data sets Home-page: https://github.com/box/genty Author: Box Author-email: oss@box.com License: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Description: genty ===== .. image:: http://opensource.box.com/badges/active.svg :target: http://opensource.box.com/badges .. image:: https://travis-ci.org/box/genty.png?branch=master :target: https://travis-ci.org/box/genty .. image:: https://img.shields.io/pypi/v/genty.svg :target: https://pypi.python.org/pypi/genty .. image:: https://img.shields.io/pypi/dm/genty.svg :target: https://pypi.python.org/pypi/genty About ----- Genty, pronounced "gen-tee", stands for "generate tests". It promotes generative testing, where a single test can execute over a variety of input. Genty makes this a breeze. For example, consider a file sample.py containing both the code under test and the tests: .. code-block:: python from genty import genty, genty_repeat, genty_dataset from unittest import TestCase # Here's the class under test class MyClass(object): def add_one(self, x): return x + 1 # Here's the test code @genty class MyClassTests(TestCase): @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) Running the ``MyClassTests`` using the default unittest runner .. code-block:: console $ python -m unittest -v sample test_add_one(0, 1) (sample.MyClassTests) ... ok test_add_one(100000, 100001) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Instead of having to write multiple independent tests for various test cases, code can be refactored and parametrized using genty! It produces readable tests. It produces maintainable tests. It produces expressive tests. Another option is running the same test multiple times. This is useful in stress tests or when exercising code looking for race conditions. This particular test .. code-block:: python @genty_repeat(3) def test_adding_one_to_zero(self): self.assertEqual(1, MyClass().add_one(0)) would be run 3 times, producing output like .. code-block:: console $ python -m unittest -v sample test_adding_one() iteration_1 (sample.MyClassTests) ... ok test_adding_one() iteration_2 (sample.MyClassTests) ... ok test_adding_one() iteration_3 (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK The 2 techniques can be combined: .. code-block:: python @genty_repeat(2) @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) There are more options to explore including naming your datasets and ``genty_args``. .. code-block:: python @genty_dataset( default_case=(0, 1), limit_case=(999, 1000), error_case=genty_args(-1, -1, is_something=False), ) def test_complex(self, value1, value2, optional_value=None, is_something=True): ... would run 3 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_complex(default_case) (sample.MyClassTests) ... ok test_complex(limit_case) (sample.MyClassTests) ... ok test_complex(error_case) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.003s OK The ``@genty_datasets`` can be chained together. This is useful, for example, if there are semantically different datasets so keeping them separate would help expressiveness. .. code-block:: python @genty_dataset(10, 100) @genty_dataset('first', 'second') def test_composing(self, parameter_value): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_composing(10) (sample.MyClassTests) ... ok test_composing(100) (sample.MyClassTests) ... ok test_composing(u'first') (sample.MyClassTests) ... ok test_composing(u'second') (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK Sometimes the parameters to a test can't be determined at module load time. For example, some test might be based on results from some http request. And first the test needs to authenticate, etc. This is supported using the ``@genty_dataprovider`` decorator like so: .. code-block:: python def setUp(self): super(MyClassTests, self).setUp() # http authentication happens # And imagine that _some_function is actually some http request self._some_function = lambda x, y: ((x + y), (x - y), (x * y)) @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_heavy_calculate(100, 1) (sample.MyClassTests) ... ok test_heavy_calculate(1000, 100) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Notice here how the name of the helper (``calculate``) is added to the names of the 2 executed test cases. Like ``@genty_dataset``, ``@genty_dataprovider`` can be chained together. Enjoy! Deferred Parameterization ------------------------- Parametrized tests where the final parameters are not determined until test execution time. It looks like so: .. code-block:: python @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated, perhaps in # some Test.setUp() method. # Let's imagine that _some_function requires that authentication. # And it returns a 3-tuple, matching that signature of # of the test(s) decorated with this 'calculate' method. return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... The ``calculate()`` method is called 2 times based on the ``@genty_dataset`` decorator, and each of it's return values define the final parameters that will be given to the method ``test_heavy(...)``. Installation ------------ To install, simply: .. code-block:: console pip install genty Contributing ------------ See `CONTRIBUTING.rst `_. Setup ~~~~~ Create a virtual environment and install packages - .. code-block:: console mkvirtualenv genty pip install -r requirements-dev.txt Testing ~~~~~~~ Run all tests using - .. code-block:: console tox The tox tests include code style checks via pep8 and pylint. The tox tests are configured to run on Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy 2.6. Copyright and License --------------------- :: Copyright 2015 Box, Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Keywords: genty,tests,generative,unittest Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Software Development Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X genty-1.3.0/README.rst0000644€BìÙ΀HÖ¢0000001734312616750352015624 0ustar jmeadows00000000000000genty ===== .. image:: http://opensource.box.com/badges/active.svg :target: http://opensource.box.com/badges .. image:: https://travis-ci.org/box/genty.png?branch=master :target: https://travis-ci.org/box/genty .. image:: https://img.shields.io/pypi/v/genty.svg :target: https://pypi.python.org/pypi/genty .. image:: https://img.shields.io/pypi/dm/genty.svg :target: https://pypi.python.org/pypi/genty About ----- Genty, pronounced "gen-tee", stands for "generate tests". It promotes generative testing, where a single test can execute over a variety of input. Genty makes this a breeze. For example, consider a file sample.py containing both the code under test and the tests: .. code-block:: python from genty import genty, genty_repeat, genty_dataset from unittest import TestCase # Here's the class under test class MyClass(object): def add_one(self, x): return x + 1 # Here's the test code @genty class MyClassTests(TestCase): @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) Running the ``MyClassTests`` using the default unittest runner .. code-block:: console $ python -m unittest -v sample test_add_one(0, 1) (sample.MyClassTests) ... ok test_add_one(100000, 100001) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Instead of having to write multiple independent tests for various test cases, code can be refactored and parametrized using genty! It produces readable tests. It produces maintainable tests. It produces expressive tests. Another option is running the same test multiple times. This is useful in stress tests or when exercising code looking for race conditions. This particular test .. code-block:: python @genty_repeat(3) def test_adding_one_to_zero(self): self.assertEqual(1, MyClass().add_one(0)) would be run 3 times, producing output like .. code-block:: console $ python -m unittest -v sample test_adding_one() iteration_1 (sample.MyClassTests) ... ok test_adding_one() iteration_2 (sample.MyClassTests) ... ok test_adding_one() iteration_3 (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK The 2 techniques can be combined: .. code-block:: python @genty_repeat(2) @genty_dataset( (0, 1), (100000, 100001), ) def test_add_one(self, value, expected_result): actual_result = MyClass().add_one(value) self.assertEqual(expected_result, actual_result) There are more options to explore including naming your datasets and ``genty_args``. .. code-block:: python @genty_dataset( default_case=(0, 1), limit_case=(999, 1000), error_case=genty_args(-1, -1, is_something=False), ) def test_complex(self, value1, value2, optional_value=None, is_something=True): ... would run 3 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_complex(default_case) (sample.MyClassTests) ... ok test_complex(limit_case) (sample.MyClassTests) ... ok test_complex(error_case) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.003s OK The ``@genty_datasets`` can be chained together. This is useful, for example, if there are semantically different datasets so keeping them separate would help expressiveness. .. code-block:: python @genty_dataset(10, 100) @genty_dataset('first', 'second') def test_composing(self, parameter_value): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_composing(10) (sample.MyClassTests) ... ok test_composing(100) (sample.MyClassTests) ... ok test_composing(u'first') (sample.MyClassTests) ... ok test_composing(u'second') (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK Sometimes the parameters to a test can't be determined at module load time. For example, some test might be based on results from some http request. And first the test needs to authenticate, etc. This is supported using the ``@genty_dataprovider`` decorator like so: .. code-block:: python def setUp(self): super(MyClassTests, self).setUp() # http authentication happens # And imagine that _some_function is actually some http request self._some_function = lambda x, y: ((x + y), (x - y), (x * y)) @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... would run 4 tests, producing output like .. code-block:: console $ python -m unittest -v sample test_heavy_calculate(100, 1) (sample.MyClassTests) ... ok test_heavy_calculate(1000, 100) (sample.MyClassTests) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK Notice here how the name of the helper (``calculate``) is added to the names of the 2 executed test cases. Like ``@genty_dataset``, ``@genty_dataprovider`` can be chained together. Enjoy! Deferred Parameterization ------------------------- Parametrized tests where the final parameters are not determined until test execution time. It looks like so: .. code-block:: python @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): # when this is called... we've been authenticated, perhaps in # some Test.setUp() method. # Let's imagine that _some_function requires that authentication. # And it returns a 3-tuple, matching that signature of # of the test(s) decorated with this 'calculate' method. return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): ... The ``calculate()`` method is called 2 times based on the ``@genty_dataset`` decorator, and each of it's return values define the final parameters that will be given to the method ``test_heavy(...)``. Installation ------------ To install, simply: .. code-block:: console pip install genty Contributing ------------ See `CONTRIBUTING.rst `_. Setup ~~~~~ Create a virtual environment and install packages - .. code-block:: console mkvirtualenv genty pip install -r requirements-dev.txt Testing ~~~~~~~ Run all tests using - .. code-block:: console tox The tox tests include code style checks via pep8 and pylint. The tox tests are configured to run on Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy 2.6. Copyright and License --------------------- :: Copyright 2015 Box, Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. genty-1.3.0/setup.cfg0000644€BìÙ΀HÖ¢0000000007312617211551015740 0ustar jmeadows00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 genty-1.3.0/setup.py0000644€BìÙ΀HÖ¢0000000345712616750352015650 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals from os.path import dirname, join from setuptools import setup, find_packages from sys import version_info CLASSIFIERS = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Topic :: Software Development', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', ] def main(): base_dir = dirname(__file__) requirements = ['six'] test_requirements = [] if version_info[0] == 2 and version_info[1] == 6: requirements.append('ordereddict') test_requirements.append('unittest2') setup( name='genty', version='1.3.0', description='Allows you to run a test with multiple data sets', long_description=open(join(base_dir, 'README.rst')).read(), author='Box', author_email='oss@box.com', url='https://github.com/box/genty', license=open(join(base_dir, 'LICENSE')).read(), packages=find_packages(exclude=['test']), test_suite='test', zip_safe=False, keywords=('genty', 'tests', 'generative', 'unittest'), classifiers=CLASSIFIERS, install_requires=requirements, tests_require=test_requirements, ) if __name__ == '__main__': main() genty-1.3.0/test/0000755€BìÙ΀HÖ¢0000000000012617211551015076 5ustar jmeadows00000000000000genty-1.3.0/test/test_case_base.py0000644€BìÙ΀HÖ¢0000000076012616747501020427 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals import six from unittest import TestCase as _TestCase if not hasattr(_TestCase, 'assertItemsEqual') and not hasattr(_TestCase, 'assertCountEqual'): # Python 2.6 support # pylint:disable=import-error from unittest2 import TestCase as _TestCase # pylint:enable=import-error class TestCase(_TestCase): if six.PY3: # pylint:disable=no-member,maybe-no-member assertItemsEqual = _TestCase.assertCountEqual genty-1.3.0/test/test_example.py0000644€BìÙ΀HÖ¢0000000651512616747501020161 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals # This is an end-to-end example of the genty package in action. Consider it # a live tutorial, showing the various features in action. from itertools import product from unittest import TestCase from genty import genty, genty_repeat, genty_dataset, genty_args, genty_dataprovider @genty class ExampleTests(TestCase): # This is set so that if nosetest's multi-processing capability is being used, the # tests in this class can be split across processes. _multiprocess_can_split_ = True def setUp(self): super(ExampleTests, self).setUp() # Imagine that _some_function is actually some http request that can # only be called *after* authentication happens in this setUp method self._some_function = lambda x, y: ((x + y), (x - y), x * y) @genty_repeat(10) def test_example_of_repeat(self): """This test will be run 10 times""" pass @genty_dataset('red', 'orange', 'blue') def test_example_of_single_parameter_datasets(self, _color): """This test will be called 3 times, each time with a different color""" pass @genty_dataset(*product([True, False], [True, False])) def test_example_of_multiple_parameter_datasets(self, _first_bool, _second_bool): """This test is called 4 times""" pass @genty_dataset( some_test_case=(10, 'happy', str), another_test_case=(7, 'sleepy', float), ) def test_example_of_named_datasets(self, value, message, kind): """This test is called for each of the 2 named datasets""" pass @genty_dataset( ('first', 'second', 'third'), genty_args('first', 'second', 'third'), genty_args('first', third_value='third', second_value='second') ) def test_example_of_datasets_with_kwargs(self, first_value, second_value=None, third_value=None): """This test is called twice, with the arguments ('first', 'second', 'third'). Note that it is not called three times, because the first and second datasets are identical.""" pass @genty_repeat(4) @genty_dataset('first', 'second', 'third') def test_example_of_repeat_and_datasets(self, parameter_value): """This test will be called 4 times for each of the 3 possible parameter_values""" pass @genty_dataset(10, 100) @genty_dataset('first', 'second', 'third') def test_example_of_composing_datasets(self, parameter_value): """This test will be called 5 times for each of the values in the 2 datasets above""" pass @genty_dataset((1000, 100), (100, 1)) def calculate(self, x_val, y_val): return self._some_function(x_val, y_val) @genty_dataprovider(calculate) def test_heavy(self, data1, data2, data3): """ This test will be called 2 times because the data_provider - the calculate helper - has 2 datasets """ def no_dataset(self): return self._some_function(100, 200) @genty_dataprovider(no_dataset) def test_dataprovider_with_no_dataset(self, data1, data2, data3): """ Uses a dataprovider that has no datasets. """ @genty_dataset('127.0.0.1') def test_with_period_char_in_dataset(self, arg): """ A dataset with a '.' doesn't screw up nosetests --processes=4 """ genty-1.3.0/test/test_genty.py0000644€BìÙ΀HÖ¢0000005047012616750010017640 0ustar jmeadows00000000000000# coding: utf-8 from __future__ import unicode_literals import functools import inspect from mock import patch import six from genty import genty, genty_args, genty_dataset, genty_repeat, genty_dataprovider from genty.genty import REPLACE_FOR_PERIOD_CHAR from genty.private import encode_non_ascii_string from test.test_case_base import TestCase class GentyTest(TestCase): """Tests for :mod:`box.test.genty.genty`.""" # pylint: disable=no-self-use # Lots of the tests below create dummy methods that don't use 'self'. def _count_test_methods(self, target_cls): method_filter = inspect.ismethod if six.PY2 else inspect.isfunction return len([ name for name, _ in inspect.getmembers(target_cls, method_filter) if name.startswith('test') ]) def test_genty_without_any_decorated_methods_is_a_no_op(self): @genty class SomeClass(object): def test_not_decorated(self): return 13 self.assertEquals(13, SomeClass().test_not_decorated()) def test_genty_ignores_non_test_methods(self): @genty class SomeClass(object): def random_method(self): return 'hi' self.assertEquals('hi', SomeClass().random_method()) def test_genty_leaves_undecorated_tests_untouched(self): @genty class SomeClass(object): def test_undecorated(self): return 15 self.assertEqual(15, SomeClass().test_undecorated()) def test_genty_decorates_test_with_args(self): @genty class SomeClass(object): @genty_dataset((4, 7)) def test_decorated(self, aval, bval): return aval + bval instance = SomeClass() self.assertEqual(11, getattr(instance, 'test_decorated(4, 7)')()) def test_genty_decorates_with_dataprovider_args(self): @genty class SomeClass(object): @genty_dataset((7, 4)) def my_param_factory(self, first, second): return first + second, first - second, max(first, second) @genty_dataprovider(my_param_factory) def test_decorated(self, summation, difference, maximum): return summation, difference, maximum instance = SomeClass() self.assertEqual( (11, 3, 7), getattr( instance, 'test_decorated_{0}(7, 4)'.format('my_param_factory'), )(), ) def test_genty_dataprovider_can_handle_single_parameter(self): @genty class SomeClass(object): @genty_dataset((7, 4)) def my_param_factory(self, first, second): return first + second @genty_dataprovider(my_param_factory) def test_decorated(self, sole_arg): return sole_arg instance = SomeClass() self.assertEqual( 11, getattr( instance, 'test_decorated_{0}(7, 4)'.format('my_param_factory'), )(), ) def test_genty_dataprovider_doesnt_need_any_datasets(self): @genty class SomeClass(object): def my_param_factory(self): return 101 @genty_dataprovider(my_param_factory) def test_decorated(self, sole_arg): return sole_arg instance = SomeClass() self.assertEqual( 101, getattr( instance, 'test_decorated_{0}'.format('my_param_factory'), )(), ) def test_genty_dataprovider_can_be_chained(self): @genty class SomeClass(object): @genty_dataset((7, 4)) def my_param_factory(self, first, second): return first + second, first - second, max(first, second) @genty_dataset(3, 5) def another_param_factory(self, only): return only + only, only - only, (only * only) @genty_dataprovider(my_param_factory) @genty_dataprovider(another_param_factory) def test_decorated(self, value1, value2, value3): return value1, value2, value3 instance = SomeClass() self.assertEqual( (11, 3, 7), getattr( instance, 'test_decorated_{0}(7, 4)'.format('my_param_factory'), )(), ) self.assertEqual( (6, 0, 9), getattr( instance, 'test_decorated_{0}(3)'.format('another_param_factory'), )(), ) self.assertEqual( (10, 0, 25), getattr( instance, 'test_decorated_{0}(5)'.format('another_param_factory'), )(), ) def test_dataprovider_args_can_use_genty_args(self): @genty class SomeClass(object): @genty_dataset( genty_args(second=5, first=15), ) def my_param_factory(self, first, second): return first + second, first - second, max(first, second) @genty_dataprovider(my_param_factory) def test_decorated(self, summation, difference, maximum): return summation, difference, maximum instance = SomeClass() self.assertEqual( (20, 10, 15), getattr( instance, 'test_decorated_{0}(first=15, second=5)'.format('my_param_factory'), )(), ) def test_dataproviders_and_datasets_can_mix(self): @genty class SomeClass(object): @genty_dataset((7, 4)) def my_param_factory(self, first, second): return first + second, first - second @genty_dataprovider(my_param_factory) @genty_dataset((7, 4), (11, 3)) def test_decorated(self, param1, param2): return param1, param1, param2, param2 instance = SomeClass() self.assertEqual( (11, 11, 3, 3), getattr( instance, 'test_decorated_{0}(7, 4)'.format('my_param_factory'), )(), ) self.assertEqual( (7, 7, 4, 4), getattr(instance, 'test_decorated(7, 4)')(), ) self.assertEqual( (11, 11, 3, 3), getattr(instance, 'test_decorated(11, 3)')(), ) def test_genty_replicates_method_based_on_repeat_count(self): @genty class SomeClass(object): @genty_repeat(2) def test_repeat_decorated(self): return 13 instance = SomeClass() # The test method should be expanded twice and the original method should be gone. self.assertEqual(2, self._count_test_methods(SomeClass)) getattr(instance, 'test_repeat_decorated() iteration_1')() self.assertEqual(13, getattr(instance, 'test_repeat_decorated() iteration_1')()) self.assertEqual(13, getattr(instance, 'test_repeat_decorated() iteration_2')()) self.assertFalse(hasattr(instance, 'test_repeat_decorated'), "original method should not exist") @patch('sys.argv', ['test_module.test_dot_argv', 'test_module:test_colon_argv']) def test_genty_generates_test_with_original_name_if_referenced_in_argv(self): @genty class SomeClass(object): @genty_repeat(3) def test_dot_argv(self): return 13 @genty_dataset(10, 11) def test_colon_argv(self, _): return 53 instance = SomeClass() # A test with the original same should exist, because of the argv reference. # And then the remaining generated tests exist as normal self.assertEqual(5, self._count_test_methods(SomeClass)) self.assertEqual(13, instance.test_dot_argv()) self.assertEqual(13, getattr(instance, 'test_dot_argv() iteration_2')()) self.assertEqual(13, getattr(instance, 'test_dot_argv() iteration_3')()) # pylint: disable=no-value-for-parameter # genty replace the original 'test_colon_argv' method with one that doesn't # take any paramteres. Hence pylint's confusion self.assertEqual(53, instance.test_colon_argv()) # pylint: enable=no-value-for-parameter self.assertEqual(53, getattr(instance, 'test_colon_argv(11)')()) def test_genty_formats_test_method_names_correctly_for_large_repeat_counts(self): @genty class SomeClass(object): @genty_repeat(100) def test_repeat_100(self): pass instance = SomeClass() self.assertEqual(100, self._count_test_methods(SomeClass)) for i in range(1, 10): self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_00{0}'.format(i))) for i in range(10, 100): self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_0{0}'.format(i))) self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_100')) def test_genty_properly_composes_dataset_methods(self): @genty class SomeClass(object): @genty_dataset( (100, 10), (200, 20), genty_args(110, 50), genty_args(val=120, other=80), genty_args(500, other=50), some_values=(250, 10), other_values=(300, 30), more_values=genty_args(400, other=40) ) def test_something(self, val, other): return val + other + 1 instance = SomeClass() self.assertEqual(8, self._count_test_methods(SomeClass)) self.assertEqual(111, getattr(instance, 'test_something(100, 10)')()) self.assertEqual(221, getattr(instance, 'test_something(200, 20)')()) self.assertEqual(161, getattr(instance, 'test_something(110, 50)')()) self.assertEqual(201, getattr(instance, 'test_something(other=80, val=120)')()) self.assertEqual(551, getattr(instance, 'test_something(500, other=50)')()) self.assertEqual(261, getattr(instance, 'test_something(some_values)')()) self.assertEqual(331, getattr(instance, 'test_something(other_values)')()) self.assertEqual(441, getattr(instance, 'test_something(more_values)')()) self.assertFalse(hasattr(instance, 'test_something'), "original method should not exist") def test_genty_properly_composes_dataset_methods_up_hierarchy(self): # Some test frameworks set attributes on test classes directly through metaclasses. pymox is an example. # This test ensures that genty still won't expand inherited tests twice. class SomeMeta(type): def __init__(cls, name, bases, d): for base in bases: for attr_name in dir(base): if attr_name not in d: d[attr_name] = getattr(base, attr_name) for func_name, func in d.items(): if func_name.startswith('test') and callable(func): setattr(cls, func_name, cls.wrap_method(func)) # pylint:disable=bad-super-call super(SomeMeta, cls).__init__(name, bases, d) def wrap_method(cls, func): @functools.wraps(func) def wrapped(*args, **kwargs): return func(*args, **kwargs) return wrapped @genty @six.add_metaclass(SomeMeta) class SomeParent(object): @genty_dataset(100, 10) def test_parent(self, val): return val + 1 @genty class SomeChild(SomeParent): @genty_dataset('a', 'b') def test_child(self, val): return val + val instance = SomeChild() self.assertEqual(4, self._count_test_methods(SomeChild)) self.assertEqual(101, getattr(instance, 'test_parent(100)')()) self.assertEqual(11, getattr(instance, 'test_parent(10)')()) self.assertEqual('aa', getattr(instance, "test_child({0})".format(repr('a')))()) self.assertEqual('bb', getattr(instance, "test_child({0})".format(repr('b')))()) entries = dict(six.iteritems(SomeChild.__dict__)) self.assertEqual(4, len([meth for name, meth in six.iteritems(entries) if name.startswith('test')])) self.assertFalse(hasattr(instance, 'test_parent(100)(100)'), 'genty should not expand a test more than once') self.assertFalse(hasattr(instance, 'test_parent(100)(10)'), 'genty should not expand a test more than once') self.assertFalse(hasattr(instance, 'test_parent(100)(10)'), 'genty should not expand a test more than once') self.assertFalse(hasattr(instance, 'test_parent(10)(10)'), 'genty should not expand a test more than once') self.assertFalse(hasattr(instance, 'test_parent'), "original method should not exist") self.assertFalse(hasattr(instance, 'test_child'), "original method should not exist") def test_genty_properly_composes_repeat_methods_up_hierarchy(self): @genty class SomeParent(object): @genty_repeat(3) def test_parent(self): return 1 + 1 @genty class SomeChild(SomeParent): @genty_repeat(2) def test_child(self): return 'r' instance = SomeChild() self.assertEqual(5, self._count_test_methods(SomeChild)) self.assertEqual(2, getattr(instance, 'test_parent() iteration_1')()) self.assertEqual(2, getattr(instance, 'test_parent() iteration_2')()) self.assertEqual(2, getattr(instance, 'test_parent() iteration_3')()) self.assertEqual('r', getattr(instance, 'test_child() iteration_1')()) self.assertEqual('r', getattr(instance, 'test_child() iteration_2')()) self.assertFalse(hasattr(instance, 'test_parent'), "original method should not exist") self.assertFalse(hasattr(instance, 'test_child'), "original method should not exist") def test_genty_replicates_method_with_repeat_then_dataset_decorators(self): @genty class SomeClass(object): @genty_repeat(2) @genty_dataset('first', 'second') def test_repeat_and_dataset(self, val): return val + val instance = SomeClass() # The test method should be expanded twice and the original method should be gone. self.assertEqual(4, self._count_test_methods(SomeClass)) self.assertEqual('firstfirst', getattr(instance, "test_repeat_and_dataset({0}) iteration_1".format(repr('first')))()) self.assertEqual('firstfirst', getattr(instance, "test_repeat_and_dataset({0}) iteration_2".format(repr('first')))()) self.assertEqual('secondsecond', getattr(instance, "test_repeat_and_dataset({0}) iteration_1".format(repr('second')))()) self.assertEqual('secondsecond', getattr(instance, "test_repeat_and_dataset({0}) iteration_2".format(repr('second')))()) self.assertFalse(hasattr(instance, 'test_repeat_and_dataset'), "original method should not exist") def test_genty_replicates_method_with_dataset_then_repeat_decorators(self): @genty class SomeClass(object): @genty_dataset(11, 22) @genty_repeat(2) def test_repeat_and_dataset(self, val): return val + 13 instance = SomeClass() # The test method should be expanded twice and the original method should be gone. self.assertEqual(4, self._count_test_methods(SomeClass)) self.assertEqual(24, getattr(instance, 'test_repeat_and_dataset(11) iteration_1')()) self.assertEqual(24, getattr(instance, 'test_repeat_and_dataset(11) iteration_2')()) self.assertEqual(35, getattr(instance, 'test_repeat_and_dataset(22) iteration_1')()) self.assertEqual(35, getattr(instance, 'test_repeat_and_dataset(22) iteration_2')()) self.assertFalse(hasattr(instance, 'test_repeat_and_dataset'), "original method should not exist") def test_genty_properly_composes_method_with_non_ascii_chars_in_dataset_name(self): @genty class SomeClass(object): @genty_dataset(' PÈ…tÈ…r', 'wow 漢字') def test_unicode(self, _): return 33 instance = SomeClass() self.assertEqual( 33, getattr(instance, encode_non_ascii_string('test_unicode({0})'.format(repr(' PÈ…tÈ…r'))))() ) self.assertEqual( 33, getattr(instance, encode_non_ascii_string('test_unicode({0})'.format(repr('wow 漢字'))))() ) def test_genty_properly_composes_method_with_special_chars_in_dataset_name(self): @genty class SomeClass(object): @genty_dataset(*r'!"#$%&\'()*+-/:;>=== 0', str(context.exception)) def test_repeat_allows_zero_iterations(self): @genty_repeat(0) def some_func(): pass self.assertEqual(0, some_func.genty_repeat_count)