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