|
6 | 6 | import threading |
7 | 7 | import time |
8 | 8 | import weakref |
9 | | -from concurrent.futures import CancelledError |
| 9 | +from concurrent.futures import CancelledError, TimeoutError |
| 10 | +from itertools import islice |
10 | 11 |
|
11 | 12 | import pytest |
12 | 13 |
|
@@ -145,3 +146,103 @@ def task(): |
145 | 146 | assert cancels > 0 |
146 | 147 | else: |
147 | 148 | assert cancels == 0 |
| 149 | + |
| 150 | + |
| 151 | +def test_map(executor): |
| 152 | + """Basic test of executor map functionality""" |
| 153 | + results = list(executor.map(lambda x: x + 1, range(10))) |
| 154 | + assert results == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
| 155 | + |
| 156 | + results = list(executor.map(lambda x, y: x + y, range(10), range(9))) |
| 157 | + assert results == [0, 2, 4, 6, 8, 10, 12, 14, 16] |
| 158 | + |
| 159 | + |
| 160 | +def test_map_timeout(executor): |
| 161 | + """Test that map with timeout raises TimeoutError and cancels futures""" |
| 162 | + results = [] |
| 163 | + |
| 164 | + def func(x): |
| 165 | + nonlocal results |
| 166 | + time.sleep(0.05) |
| 167 | + results.append(x) |
| 168 | + return x |
| 169 | + |
| 170 | + start = time.monotonic() |
| 171 | + with pytest.raises(TimeoutError): |
| 172 | + list(executor.map(func, range(10), timeout=0.01)) |
| 173 | + duration = time.monotonic() - start |
| 174 | + # this test is flaky on some platforms, so we give it a wide bearth. |
| 175 | + assert duration < 0.1 |
| 176 | + |
| 177 | + executor.shutdown(wait=True) |
| 178 | + # only about half of the tasks should have completed |
| 179 | + # because the max number of workers is 5 and the rest of |
| 180 | + # the tasks were not started at the time of the cancel. |
| 181 | + assert set(results) != {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
| 182 | + |
| 183 | + |
| 184 | +def test_map_error(executor): |
| 185 | + """Test that map with an exception will raise, and remaining tasks are cancelled""" |
| 186 | + results = [] |
| 187 | + |
| 188 | + def func(x): |
| 189 | + nonlocal results |
| 190 | + time.sleep(0.05) |
| 191 | + if len(results) == 5: |
| 192 | + raise ValueError("Test error") |
| 193 | + results.append(x) |
| 194 | + return x |
| 195 | + |
| 196 | + with pytest.raises(ValueError): |
| 197 | + list(executor.map(func, range(15))) |
| 198 | + |
| 199 | + executor.shutdown(wait=True, cancel_futures=False) |
| 200 | + assert len(results) <= 10, "Final 5 at least should have been cancelled" |
| 201 | + |
| 202 | + |
| 203 | +@pytest.mark.parametrize("cancel", [True, False]) |
| 204 | +def test_map_shutdown(executor, cancel): |
| 205 | + results = [] |
| 206 | + |
| 207 | + def func(x): |
| 208 | + nonlocal results |
| 209 | + time.sleep(0.05) |
| 210 | + results.append(x) |
| 211 | + return x |
| 212 | + |
| 213 | + # Get the first few results. |
| 214 | + # Keep the iterator alive so that it isn't closed when its reference is dropped. |
| 215 | + m = executor.map(func, range(15)) |
| 216 | + values = list(islice(m, 5)) |
| 217 | + assert values == [0, 1, 2, 3, 4] |
| 218 | + |
| 219 | + executor.shutdown(wait=True, cancel_futures=cancel) |
| 220 | + if cancel: |
| 221 | + assert len(results) < 15, "Some tasks should have been cancelled" |
| 222 | + else: |
| 223 | + assert len(results) == 15, "All tasks should have been completed" |
| 224 | + m.close() |
| 225 | + |
| 226 | + |
| 227 | +def test_map_start(executor): |
| 228 | + """Test that map starts tasks immediately, before iterating""" |
| 229 | + e = threading.Event() |
| 230 | + m = executor.map(lambda x: (e.set(), x), range(1)) |
| 231 | + e.wait(timeout=0.1) |
| 232 | + assert list(m) == [(None, 0)] |
| 233 | + |
| 234 | + |
| 235 | +def test_map_close(executor): |
| 236 | + """Test that closing a running map cancels all remaining tasks.""" |
| 237 | + results = [] |
| 238 | + def func(x): |
| 239 | + nonlocal results |
| 240 | + time.sleep(0.05) |
| 241 | + results.append(x) |
| 242 | + return x |
| 243 | + m = executor.map(func, range(10)) |
| 244 | + # must start the generator so that close() has any effect |
| 245 | + assert next(m) == 0 |
| 246 | + m.close() |
| 247 | + executor.shutdown(wait=True, cancel_futures=False) |
| 248 | + assert len(results) < 10, "Some tasks should have been cancelled" |
0 commit comments