1# coding: utf-8
2
3from __future__ import unicode_literals
4import functools
5import inspect
6from mock import patch
7import six
8from genty import genty, genty_args, genty_dataset, genty_repeat, genty_dataprovider
9from genty.genty import REPLACE_FOR_PERIOD_CHAR
10from genty.private import encode_non_ascii_string
11from test.test_case_base import TestCase
12
13
14class GentyTest(TestCase):
15    """Tests for :mod:`box.test.genty.genty`."""
16    # pylint: disable=no-self-use
17    # Lots of the tests below create dummy methods that don't use 'self'.
18
19    def _count_test_methods(self, target_cls):
20        method_filter = inspect.ismethod if six.PY2 else inspect.isfunction
21        return len([
22            name for name, _ in inspect.getmembers(target_cls, method_filter)
23            if name.startswith('test')
24        ])
25
26    def test_genty_without_any_decorated_methods_is_a_no_op(self):
27        @genty
28        class SomeClass(object):
29            def test_not_decorated(self):
30                return 13
31
32        self.assertEqual(13, SomeClass().test_not_decorated())
33
34    def test_genty_ignores_non_test_methods(self):
35        @genty
36        class SomeClass(object):
37            def random_method(self):
38                return 'hi'
39
40        self.assertEqual('hi', SomeClass().random_method())
41
42    def test_genty_leaves_undecorated_tests_untouched(self):
43        @genty
44        class SomeClass(object):
45            def test_undecorated(self):
46                return 15
47
48        self.assertEqual(15, SomeClass().test_undecorated())
49
50    def test_genty_decorates_test_with_args(self):
51        @genty
52        class SomeClass(object):
53            @genty_dataset((4, 7))
54            def test_decorated(self, aval, bval):
55                return aval + bval
56
57        instance = SomeClass()
58        self.assertEqual(11, getattr(instance, 'test_decorated(4, 7)')())
59
60    def test_genty_decorates_with_dataprovider_args(self):
61        @genty
62        class SomeClass(object):
63            @genty_dataset((7, 4))
64            def my_param_factory(self, first, second):
65                return first + second, first - second, max(first, second)
66
67            @genty_dataprovider(my_param_factory)
68            def test_decorated(self, summation, difference, maximum):
69                return summation, difference, maximum
70
71        instance = SomeClass()
72        self.assertEqual(
73            (11, 3, 7),
74            getattr(
75                instance,
76                'test_decorated_{0}(7, 4)'.format('my_param_factory'),
77            )(),
78        )
79
80    def test_genty_dataprovider_can_handle_single_parameter(self):
81        @genty
82        class SomeClass(object):
83            @genty_dataset((7, 4))
84            def my_param_factory(self, first, second):
85                return first + second
86
87            @genty_dataprovider(my_param_factory)
88            def test_decorated(self, sole_arg):
89                return sole_arg
90
91        instance = SomeClass()
92        self.assertEqual(
93            11,
94            getattr(
95                instance,
96                'test_decorated_{0}(7, 4)'.format('my_param_factory'),
97            )(),
98        )
99
100    def test_genty_dataprovider_doesnt_need_any_datasets(self):
101        @genty
102        class SomeClass(object):
103            def my_param_factory(self):
104                return 101
105
106            @genty_dataprovider(my_param_factory)
107            def test_decorated(self, sole_arg):
108                return sole_arg
109
110        instance = SomeClass()
111        self.assertEqual(
112            101,
113            getattr(
114                instance,
115                'test_decorated_{0}'.format('my_param_factory'),
116            )(),
117        )
118
119    def test_genty_dataprovider_can_be_chained(self):
120        @genty
121        class SomeClass(object):
122            @genty_dataset((7, 4))
123            def my_param_factory(self, first, second):
124                return first + second, first - second, max(first, second)
125
126            @genty_dataset(3, 5)
127            def another_param_factory(self, only):
128                return only + only, only - only, (only * only)
129
130            @genty_dataprovider(my_param_factory)
131            @genty_dataprovider(another_param_factory)
132            def test_decorated(self, value1, value2, value3):
133                return value1, value2, value3
134
135        instance = SomeClass()
136        self.assertEqual(
137            (11, 3, 7),
138            getattr(
139                instance,
140                'test_decorated_{0}(7, 4)'.format('my_param_factory'),
141            )(),
142        )
143        self.assertEqual(
144            (6, 0, 9),
145            getattr(
146                instance,
147                'test_decorated_{0}(3)'.format('another_param_factory'),
148            )(),
149        )
150        self.assertEqual(
151            (10, 0, 25),
152            getattr(
153                instance,
154                'test_decorated_{0}(5)'.format('another_param_factory'),
155            )(),
156        )
157
158    def test_dataprovider_args_can_use_genty_args(self):
159        @genty
160        class SomeClass(object):
161            @genty_dataset(
162                genty_args(second=5, first=15),
163            )
164            def my_param_factory(self, first, second):
165                return first + second, first - second, max(first, second)
166
167            @genty_dataprovider(my_param_factory)
168            def test_decorated(self, summation, difference, maximum):
169                return summation, difference, maximum
170
171        instance = SomeClass()
172        self.assertEqual(
173            (20, 10, 15),
174            getattr(
175                instance,
176                'test_decorated_{0}(first=15, second=5)'.format('my_param_factory'),
177            )(),
178        )
179
180    def test_dataproviders_and_datasets_can_mix(self):
181        @genty
182        class SomeClass(object):
183            @genty_dataset((7, 4))
184            def my_param_factory(self, first, second):
185                return first + second, first - second
186
187            @genty_dataprovider(my_param_factory)
188            @genty_dataset((7, 4), (11, 3))
189            def test_decorated(self, param1, param2):
190                return param1, param1, param2, param2
191
192        instance = SomeClass()
193        self.assertEqual(
194            (11, 11, 3, 3),
195            getattr(
196                instance,
197                'test_decorated_{0}(7, 4)'.format('my_param_factory'),
198            )(),
199        )
200        self.assertEqual(
201            (7, 7, 4, 4),
202            getattr(instance, 'test_decorated(7, 4)')(),
203        )
204        self.assertEqual(
205            (11, 11, 3, 3),
206            getattr(instance, 'test_decorated(11, 3)')(),
207        )
208
209    def test_genty_replicates_method_based_on_repeat_count(self):
210        @genty
211        class SomeClass(object):
212            @genty_repeat(2)
213            def test_repeat_decorated(self):
214                return 13
215
216        instance = SomeClass()
217
218        # The test method should be expanded twice and the original method should be gone.
219        self.assertEqual(2, self._count_test_methods(SomeClass))
220        getattr(instance, 'test_repeat_decorated() iteration_1')()
221        self.assertEqual(13, getattr(instance, 'test_repeat_decorated() iteration_1')())
222        self.assertEqual(13, getattr(instance, 'test_repeat_decorated() iteration_2')())
223
224        self.assertFalse(hasattr(instance, 'test_repeat_decorated'), "original method should not exist")
225
226    @patch('sys.argv', ['test_module.test_dot_argv', 'test_module:test_colon_argv'])
227    def test_genty_generates_test_with_original_name_if_referenced_in_argv(self):
228
229        @genty
230        class SomeClass(object):
231            @genty_repeat(3)
232            def test_dot_argv(self):
233                return 13
234
235            @genty_dataset(10, 11)
236            def test_colon_argv(self, _):
237                return 53
238
239        instance = SomeClass()
240
241        # A test with the original same should exist, because of the argv reference.
242        # And then the remaining generated tests exist as normal
243        self.assertEqual(5, self._count_test_methods(SomeClass))
244        self.assertEqual(13, instance.test_dot_argv())
245        self.assertEqual(13, getattr(instance, 'test_dot_argv() iteration_2')())
246        self.assertEqual(13, getattr(instance, 'test_dot_argv() iteration_3')())
247
248        # pylint: disable=no-value-for-parameter
249        # genty replace the original 'test_colon_argv' method with one that doesn't
250        # take any paramteres. Hence pylint's confusion
251        self.assertEqual(53, instance.test_colon_argv())
252        # pylint: enable=no-value-for-parameter
253        self.assertEqual(53, getattr(instance, 'test_colon_argv(11)')())
254
255    def test_genty_formats_test_method_names_correctly_for_large_repeat_counts(self):
256        @genty
257        class SomeClass(object):
258            @genty_repeat(100)
259            def test_repeat_100(self):
260                pass
261
262        instance = SomeClass()
263
264        self.assertEqual(100, self._count_test_methods(SomeClass))
265        for i in range(1, 10):
266            self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_00{0}'.format(i)))
267        for i in range(10, 100):
268            self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_0{0}'.format(i)))
269        self.assertTrue(hasattr(instance, 'test_repeat_100() iteration_100'))
270
271    def test_genty_properly_composes_dataset_methods(self):
272        @genty
273        class SomeClass(object):
274            @genty_dataset(
275                (100, 10),
276                (200, 20),
277                genty_args(110, 50),
278                genty_args(val=120, other=80),
279                genty_args(500, other=50),
280                some_values=(250, 10),
281                other_values=(300, 30),
282                more_values=genty_args(400, other=40)
283            )
284            def test_something(self, val, other):
285                return val + other + 1
286
287        instance = SomeClass()
288
289        self.assertEqual(8, self._count_test_methods(SomeClass))
290        self.assertEqual(111, getattr(instance, 'test_something(100, 10)')())
291        self.assertEqual(221, getattr(instance, 'test_something(200, 20)')())
292        self.assertEqual(161, getattr(instance, 'test_something(110, 50)')())
293        self.assertEqual(201, getattr(instance, 'test_something(other=80, val=120)')())
294        self.assertEqual(551, getattr(instance, 'test_something(500, other=50)')())
295        self.assertEqual(261, getattr(instance, 'test_something(some_values)')())
296        self.assertEqual(331, getattr(instance, 'test_something(other_values)')())
297        self.assertEqual(441, getattr(instance, 'test_something(more_values)')())
298
299        self.assertFalse(hasattr(instance, 'test_something'), "original method should not exist")
300
301    def test_genty_properly_composes_dataset_methods_up_hierarchy(self):
302        # Some test frameworks set attributes on test classes directly through metaclasses. pymox is an example.
303        # This test ensures that genty still won't expand inherited tests twice.
304        class SomeMeta(type):
305            def __init__(cls, name, bases, d):
306                for base in bases:
307                    for attr_name in dir(base):
308                        if attr_name not in d:
309                            d[attr_name] = getattr(base, attr_name)
310
311                for func_name, func in d.items():
312                    if func_name.startswith('test') and callable(func):
313                        setattr(cls, func_name, cls.wrap_method(func))
314
315                # pylint:disable=bad-super-call
316                super(SomeMeta, cls).__init__(name, bases, d)
317
318            def wrap_method(cls, func):
319                @functools.wraps(func)
320                def wrapped(*args, **kwargs):
321                    return func(*args, **kwargs)
322                return wrapped
323
324        @genty
325        @six.add_metaclass(SomeMeta)
326        class SomeParent(object):
327            @genty_dataset(100, 10)
328            def test_parent(self, val):
329                return val + 1
330
331        @genty
332        class SomeChild(SomeParent):
333            @genty_dataset('a', 'b')
334            def test_child(self, val):
335                return val + val
336
337        instance = SomeChild()
338
339        self.assertEqual(4, self._count_test_methods(SomeChild))
340        self.assertEqual(101, getattr(instance, 'test_parent(100)')())
341        self.assertEqual(11, getattr(instance, 'test_parent(10)')())
342        self.assertEqual('aa', getattr(instance, "test_child({0})".format(repr('a')))())
343        self.assertEqual('bb', getattr(instance, "test_child({0})".format(repr('b')))())
344
345        entries = dict(six.iteritems(SomeChild.__dict__))
346        self.assertEqual(4, len([meth for name, meth in six.iteritems(entries) if name.startswith('test')]))
347        self.assertFalse(hasattr(instance, 'test_parent(100)(100)'), 'genty should not expand a test more than once')
348        self.assertFalse(hasattr(instance, 'test_parent(100)(10)'), 'genty should not expand a test more than once')
349        self.assertFalse(hasattr(instance, 'test_parent(100)(10)'), 'genty should not expand a test more than once')
350        self.assertFalse(hasattr(instance, 'test_parent(10)(10)'), 'genty should not expand a test more than once')
351
352        self.assertFalse(hasattr(instance, 'test_parent'), "original method should not exist")
353        self.assertFalse(hasattr(instance, 'test_child'), "original method should not exist")
354
355    def test_genty_properly_composes_repeat_methods_up_hierarchy(self):
356        @genty
357        class SomeParent(object):
358            @genty_repeat(3)
359            def test_parent(self):
360                return 1 + 1
361
362        @genty
363        class SomeChild(SomeParent):
364            @genty_repeat(2)
365            def test_child(self):
366                return 'r'
367
368        instance = SomeChild()
369
370        self.assertEqual(5, self._count_test_methods(SomeChild))
371
372        self.assertEqual(2, getattr(instance, 'test_parent() iteration_1')())
373        self.assertEqual(2, getattr(instance, 'test_parent() iteration_2')())
374        self.assertEqual(2, getattr(instance, 'test_parent() iteration_3')())
375        self.assertEqual('r', getattr(instance, 'test_child() iteration_1')())
376        self.assertEqual('r', getattr(instance, 'test_child() iteration_2')())
377
378        self.assertFalse(hasattr(instance, 'test_parent'), "original method should not exist")
379        self.assertFalse(hasattr(instance, 'test_child'), "original method should not exist")
380
381    def test_genty_replicates_method_with_repeat_then_dataset_decorators(self):
382        @genty
383        class SomeClass(object):
384            @genty_repeat(2)
385            @genty_dataset('first', 'second')
386            def test_repeat_and_dataset(self, val):
387                return val + val
388
389        instance = SomeClass()
390
391        # The test method should be expanded twice and the original method should be gone.
392        self.assertEqual(4, self._count_test_methods(SomeClass))
393        self.assertEqual('firstfirst', getattr(instance, "test_repeat_and_dataset({0}) iteration_1".format(repr('first')))())
394        self.assertEqual('firstfirst', getattr(instance, "test_repeat_and_dataset({0}) iteration_2".format(repr('first')))())
395        self.assertEqual('secondsecond', getattr(instance, "test_repeat_and_dataset({0}) iteration_1".format(repr('second')))())
396        self.assertEqual('secondsecond', getattr(instance, "test_repeat_and_dataset({0}) iteration_2".format(repr('second')))())
397
398        self.assertFalse(hasattr(instance, 'test_repeat_and_dataset'), "original method should not exist")
399
400    def test_genty_replicates_method_with_dataset_then_repeat_decorators(self):
401        @genty
402        class SomeClass(object):
403            @genty_dataset(11, 22)
404            @genty_repeat(2)
405            def test_repeat_and_dataset(self, val):
406                return val + 13
407
408        instance = SomeClass()
409
410        # The test method should be expanded twice and the original method should be gone.
411        self.assertEqual(4, self._count_test_methods(SomeClass))
412        self.assertEqual(24, getattr(instance, 'test_repeat_and_dataset(11) iteration_1')())
413        self.assertEqual(24, getattr(instance, 'test_repeat_and_dataset(11) iteration_2')())
414        self.assertEqual(35, getattr(instance, 'test_repeat_and_dataset(22) iteration_1')())
415        self.assertEqual(35, getattr(instance, 'test_repeat_and_dataset(22) iteration_2')())
416
417        self.assertFalse(hasattr(instance, 'test_repeat_and_dataset'), "original method should not exist")
418
419    def test_genty_properly_composes_method_with_non_ascii_chars_in_dataset_name(self):
420        @genty
421        class SomeClass(object):
422            @genty_dataset(' Pȅtȅr', 'wow 漢字')
423            def test_unicode(self, _):
424                return 33
425
426        instance = SomeClass()
427
428        self.assertEqual(
429            33,
430            getattr(instance, encode_non_ascii_string('test_unicode({0})'.format(repr(' Pȅtȅr'))))()
431        )
432
433        self.assertEqual(
434            33,
435            getattr(instance, encode_non_ascii_string('test_unicode({0})'.format(repr('wow 漢字'))))()
436        )
437
438    def test_genty_properly_composes_method_with_special_chars_in_dataset_name(self):
439        @genty
440        class SomeClass(object):
441            @genty_dataset(*r'!"#$%&\'()*+-/:;>=<?@[\]^_`{|}~,')
442            def test_unicode(self, _):
443                return 33
444
445        instance = SomeClass()
446
447        for char in r'!"#$%&\'()*+-/:;>=<?@[\]^_`{|}~,':
448            self.assertEqual(
449                33,
450                getattr(instance, 'test_unicode({0})'.format(repr(char)))()
451            )
452
453    def test_genty_replaces_standard_period_with_middle_dot(self):
454        # The nosetest multi-processing code parses the full test name
455        # to discern package/module names. Thus any periods in the test-name
456        # causes that code to fail. This test verifies that periods are replaced
457        # with the unicode middle-dot character.
458        @genty
459        class SomeClass(object):
460            @genty_dataset('a.b.c')
461            def test_period_char(self, _):
462                return 33
463
464        instance = SomeClass()
465
466        for attr in dir(instance):
467            if attr.startswith(encode_non_ascii_string('test_period_char')):
468                self.assertNotIn(
469                    encode_non_ascii_string('.'),
470                    attr,
471                    "didn't expect a period character",
472                )
473                self.assertIn(
474                    encode_non_ascii_string(REPLACE_FOR_PERIOD_CHAR),
475                    attr,
476                    "expected the middle-dot replacement character",
477                )
478                break
479        else:
480            raise KeyError("failed to find the expected test")
481
482    def test_genty_properly_calls_patched_methods(self):
483        class PatchableClass(object):
484            @staticmethod
485            def my_method(num):
486                return num + 1
487
488        @genty
489        class SomeClass(object):
490            @genty_dataset(42)
491            @patch.object(PatchableClass, 'my_method')
492            def test_patched_method(self, num, mocked_method):
493                mocked_method.return_value = num + 2
494                return PatchableClass.my_method(num)
495
496            @genty_dataset(42)
497            def test_unpatched_method(self, num):
498                return PatchableClass.my_method(num)
499
500        instance = SomeClass()
501        patched_method = getattr(instance, 'test_patched_method(42)')
502        unpatched_method = getattr(instance, 'test_unpatched_method(42)')
503        self.assertEqual(44, patched_method())
504        self.assertEqual(43, unpatched_method())
505
506    def test_genty_does_not_fail_when_trying_to_delete_attribute_defined_on_metaclass(self):
507        class SomeMeta(type):
508            def __new__(mcs, name, bases, attributes):
509                attributes['test_defined_in_metaclass'] = genty_dataset('foo')(mcs.test_defined_in_metaclass)
510                # pylint:disable=bad-super-call
511                generated_class = super(SomeMeta, mcs).__new__(mcs, name, bases, attributes)
512                return generated_class
513
514            @staticmethod
515            def test_defined_in_metaclass():
516                pass
517
518        @genty
519        @six.add_metaclass(SomeMeta)
520        class SomeClass(object):
521            pass
522
523        instance = SomeClass()
524
525        self.assertIn('test_defined_in_metaclass({0})'.format(repr('foo')), dir(instance))
526
527    def test_dataprovider_returning_genty_args_passes_correct_args(self):
528        @genty
529        class TestClass(object):
530            def builder(self):
531                return genty_args(42, named='named_arg')
532
533            @genty_dataprovider(builder)
534            def test_method(self, number, default=None, named=None):
535                return number, default, named
536
537        instance = TestClass()
538        # pylint:disable=no-member
539        self.assertItemsEqual((42, None, 'named_arg'), instance.test_method_builder())
540