|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | + |
| 3 | +import collections |
| 4 | +import operator |
| 5 | +import random |
| 6 | + |
| 7 | + |
| 8 | +""" |
| 9 | +`bucket` is a string representing the bucket in which the item falls based on user's chosen |
| 10 | +bucket type. |
| 11 | +
|
| 12 | +`disabled` is either a falsey value to mark that the item is ready for shuffling (shuffling is not disabled), |
| 13 | +or a truthy value in which case the item won't be shuffled among other items with the same key. |
| 14 | +
|
| 15 | +In some cases it is important for the `disabled` to be more than just True in order |
| 16 | +to preserve a distinct disabled sub-bucket within a larger bucket and not mix it up with another |
| 17 | +disabled sub-bucket of the same larger bucket. |
| 18 | +""" |
| 19 | +ItemKey = collections.namedtuple('ItemKey', field_names=('bucket', 'disabled', 'x')) |
| 20 | +ItemKey.__new__.__defaults__ = (None, None) |
| 21 | + |
| 22 | + |
| 23 | +def _shuffle_items(items, bucket_key=None, disable=None, _shuffle_buckets=True): |
| 24 | + """ |
| 25 | + Shuffles a list of `items` in place. |
| 26 | +
|
| 27 | + If `bucket_key` is None, items are shuffled across the entire list. |
| 28 | +
|
| 29 | + `bucket_key` is an optional function called for each item in `items` to |
| 30 | + calculate the key of bucket in which the item falls. |
| 31 | +
|
| 32 | + Bucket defines the boundaries across which items will not |
| 33 | + be shuffled. |
| 34 | +
|
| 35 | + If `disable` is function and returns True for ALL items |
| 36 | + in a bucket, items in this bucket will remain in their original order. |
| 37 | +
|
| 38 | + `_shuffle_buckets` is for testing only. Setting it to False may not produce |
| 39 | + the outcome you'd expect in all scenarios because if two non-contiguous sections of items belong |
| 40 | + to the same bucket, the items in these sections will be reshuffled as if they all belonged |
| 41 | + to the first section. |
| 42 | + Example: |
| 43 | + [A1, A2, B1, B2, A3, A4] |
| 44 | +
|
| 45 | + where letter denotes bucket key, |
| 46 | + with _shuffle_buckets=False may be reshuffled to: |
| 47 | + [B2, B1, A3, A1, A4, A2] |
| 48 | +
|
| 49 | + or as well to: |
| 50 | + [A3, A2, A4, A1, B1, B2] |
| 51 | +
|
| 52 | + because all A's belong to the same bucket and will be grouped together. |
| 53 | + """ |
| 54 | + |
| 55 | + # If `bucket_key` is falsey, shuffle is global. |
| 56 | + if not bucket_key and not disable: |
| 57 | + random.shuffle(items) |
| 58 | + return |
| 59 | + |
| 60 | + def get_full_bucket_key(item): |
| 61 | + assert bucket_key or disable |
| 62 | + if bucket_key and disable: |
| 63 | + return ItemKey(bucket=bucket_key(item), disabled=disable(item)) |
| 64 | + elif disable: |
| 65 | + return ItemKey(disabled=disable(item)) |
| 66 | + else: |
| 67 | + return ItemKey(bucket=bucket_key(item)) |
| 68 | + |
| 69 | + # For a sequence of items A1, A2, B1, B2, C1, C2, |
| 70 | + # where key(A1) == key(A2) == key(C1) == key(C2), |
| 71 | + # items A1, A2, C1, and C2 will end up in the same bucket. |
| 72 | + buckets = collections.OrderedDict() |
| 73 | + for item in items: |
| 74 | + full_bucket_key = get_full_bucket_key(item) |
| 75 | + if full_bucket_key not in buckets: |
| 76 | + buckets[full_bucket_key] = [] |
| 77 | + buckets[full_bucket_key].append(item) |
| 78 | + |
| 79 | + # Shuffle inside a bucket |
| 80 | + for bucket in buckets.keys(): |
| 81 | + if not bucket.disabled: |
| 82 | + random.shuffle(buckets[bucket]) |
| 83 | + |
| 84 | + # Shuffle buckets |
| 85 | + bucket_keys = list(buckets.keys()) |
| 86 | + random.shuffle(bucket_keys) |
| 87 | + |
| 88 | + items[:] = [item for bk in bucket_keys for item in buckets[bk]] |
| 89 | + return |
| 90 | + |
| 91 | + |
| 92 | +def _get_set_of_item_ids(items): |
| 93 | + s = {} |
| 94 | + try: |
| 95 | + s = set(item.nodeid for item in items) |
| 96 | + finally: |
| 97 | + return s |
| 98 | + |
| 99 | + |
| 100 | +_is_random_order_disabled = operator.attrgetter('pytest.mark.random_order_disabled') |
| 101 | + |
| 102 | + |
| 103 | +def _disable(item): |
| 104 | + try: |
| 105 | + # In actual test runs, this is returned as a truthy instance of MarkDecorator even when you don't have |
| 106 | + # set the marker. This is a hack. |
| 107 | + is_disabled = _is_random_order_disabled(item.module) |
| 108 | + if is_disabled and is_disabled is True: |
| 109 | + # It is not enough to return just True because in case the shuffling |
| 110 | + # is disabled on module, we must preserve the module unchanged |
| 111 | + # even when the bucket type for this test run is say package or global. |
| 112 | + return item.module.__name__ |
| 113 | + except AttributeError: |
| 114 | + pass |
| 115 | + return False |
0 commit comments