-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtest_map.py
More file actions
179 lines (161 loc) · 6.17 KB
/
Copy pathtest_map.py
File metadata and controls
179 lines (161 loc) · 6.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
import sys
from pickle import PickleError
import time
from typing import Any, Callable, Iterable, List, Union
import pytest
from streamable import stream
from tests.tools.func import (
async_identity,
async_identity_sleep,
async_inverse_sleep,
async_randomly_slowed,
async_square,
identity,
identity_sleep,
inverse,
inverse_sleep,
randomly_slowed,
square,
)
from tests.tools.iter import (
ITERABLE_TYPES,
IterableType,
alist_or_list,
anext_or_next,
aiter_or_iter,
)
from tests.tools.source import N, INTEGERS, ints
def test_map_async_func_with_executor():
with pytest.raises(
TypeError,
match="`concurrency` must be an int if `into` is a coroutine function but got",
):
ints.map(async_identity, concurrency=ThreadPoolExecutor(2))
@pytest.mark.parametrize("concurrency", [1, 2, N * 2])
@pytest.mark.parametrize(
"randomly_slowed_square",
[randomly_slowed(square), async_randomly_slowed(async_square)],
)
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_map_preserves_order(
concurrency: int, itype: IterableType, randomly_slowed_square: Callable[..., Any]
) -> None:
s = ints.map(randomly_slowed_square, concurrency=concurrency)
assert alist_or_list(s, itype) == list(map(square, INTEGERS))
@pytest.mark.parametrize("as_completed, order", [(False, identity), (True, sorted)])
@pytest.mark.parametrize(
"identity_sleep, concurrency",
[
(identity_sleep, 2),
(identity_sleep, ProcessPoolExecutor(2)),
(async_identity_sleep, 2),
],
)
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_map_concurrency_ordering(
as_completed: bool,
concurrency: Union[int, Executor],
identity_sleep: Callable[..., Any],
order: Callable[[Iterable], Iterable],
itype: IterableType,
) -> None:
sleeps = [0.01, 1, 0.5]
s = stream(sleeps).map(
identity_sleep, concurrency=concurrency, as_completed=as_completed
)
assert alist_or_list(s, itype) == list(order(sleeps))
@pytest.mark.parametrize("as_completed", [False, True])
@pytest.mark.parametrize("concurrency", [1, 2, 3])
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_map_process_concurrency_partial_iteration(
as_completed: bool, concurrency: int, itype: IterableType
) -> None:
"""
Process-based concurrency should exit properly even if the stream is not exhausted.
Pending tasks should run until completion before exiting the executor context.
"""
sleeps = list(range(1, 10))
start = time.perf_counter()
with ProcessPoolExecutor(max_workers=concurrency) as processes:
s = stream(sleeps).do(
time.sleep, concurrency=processes, as_completed=as_completed
)
it = aiter_or_iter(s, itype)
# this will start the execution of `concurrency` sleeps (`sleeps[:concurrency]``)
assert anext_or_next(it, itype) == sleeps[0]
assert time.perf_counter() - start == pytest.approx(1, rel=0.15)
# now that the first sleep is done, the last one can start (`sleeps[concurrency]`)
# we exit the context manager only when all pending tasks are completed.
assert time.perf_counter() - start == pytest.approx(
sleeps[0] + sleeps[concurrency], rel=0.2
)
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9+")
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_process_concurrency_raises_on_unserializable_functions(
itype: IterableType,
) -> None:
def local_identity(x):
return x # pragma: no cover
with ProcessPoolExecutor(max_workers=2) as processes:
for f in [lambda x: x, local_identity]:
with pytest.raises((AttributeError, PickleError), match="<locals>"):
alist_or_list(ints.map(f, concurrency=processes), itype)
@pytest.mark.parametrize(
"concurrent, as_completed, expected_results",
(
(False, False, [float("inf"), 1.0, float("inf"), 0.5, float("inf")]),
(True, False, [float("inf"), 1.0, float("inf"), 0.5, float("inf")]),
(True, True, [float("inf"), float("inf"), float("inf"), 0.5, 1.0]),
),
)
@pytest.mark.parametrize("identity_sleep", [identity_sleep, async_identity_sleep])
@pytest.mark.parametrize("inverse_sleep", [inverse_sleep, async_inverse_sleep])
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_map_with_errors(
itype: IterableType,
concurrent: bool,
as_completed: bool,
expected_results: List[float],
identity_sleep: Callable[..., Any],
inverse_sleep: Callable[..., Any],
) -> None:
"""Map should be resilient to errors happening upstream or in the transformation function itself."""
# upstream errors
concurrency = 2 if concurrent else 1
s = (
stream([0, 1, 0, 2, 0])
.map(inverse)
.map(identity_sleep, concurrency=concurrency, as_completed=as_completed)
.catch(ZeroDivisionError, replace=lambda e: float("inf"))
)
assert alist_or_list(s, itype) == expected_results
# map errors
if concurrent:
# when the error is upstream, it virtually increases the concurrency by 1
# adds 1 to the concurrency to get the same behavior as when the error is in the transform function
concurrency += 1
s = (
stream([0, 1, 0, 2, 0])
.map(inverse_sleep, concurrency=concurrency, as_completed=as_completed)
.catch(ZeroDivisionError, replace=lambda e: float("inf"))
)
assert alist_or_list(s, itype) == expected_results
@pytest.mark.parametrize("concurrency, pulled_elements", [(1, 1), (2, 3)])
@pytest.mark.parametrize("identity", [identity, async_identity])
@pytest.mark.parametrize("itype", ITERABLE_TYPES)
def test_map_concurrent_buffersize(
itype: IterableType,
concurrency: int,
pulled_elements: int,
identity: Callable[..., Any],
) -> None:
"""
Non concurrent map only pulls 1 element at a time,
Concurrent map pulls `concurrency + 1` elements.
"""
src = iter(range(10))
s = stream(src).map(identity, concurrency=concurrency)
it = aiter_or_iter(s, itype)
assert anext_or_next(it, itype) == 0
assert next(src) == pulled_elements