@ajksunkang-aios/kgraph-linux-x64 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kgraph-launcher +15 -3
- package/lib/kgraph/scripts/build-bundle.sh +17 -4
- package/lib/site-packages/outcome/__init__.py +20 -0
- package/lib/site-packages/outcome/_impl.py +239 -0
- package/lib/site-packages/outcome/_util.py +33 -0
- package/lib/site-packages/outcome/_version.py +7 -0
- package/lib/site-packages/outcome/py.typed +0 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE +3 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/METADATA +63 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/RECORD +13 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/WHEEL +6 -0
- package/lib/site-packages/outcome-1.3.0.post0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sniffio/__init__.py +17 -0
- package/lib/site-packages/sniffio/_impl.py +95 -0
- package/lib/site-packages/sniffio/_tests/__init__.py +0 -0
- package/lib/site-packages/sniffio/_tests/test_sniffio.py +84 -0
- package/lib/site-packages/sniffio/_version.py +3 -0
- package/lib/site-packages/sniffio/py.typed +0 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE +3 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.MIT +20 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/METADATA +104 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/RECORD +14 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/WHEEL +5 -0
- package/lib/site-packages/sniffio-1.3.1.dist-info/top_level.txt +1 -0
- package/lib/site-packages/sortedcontainers/__init__.py +74 -0
- package/lib/site-packages/sortedcontainers/sorteddict.py +812 -0
- package/lib/site-packages/sortedcontainers/sortedlist.py +2646 -0
- package/lib/site-packages/sortedcontainers/sortedset.py +733 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/LICENSE +13 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/METADATA +264 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/RECORD +10 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/WHEEL +6 -0
- package/lib/site-packages/sortedcontainers-2.4.0.dist-info/top_level.txt +1 -0
- package/lib/site-packages/trio/__init__.py +133 -0
- package/lib/site-packages/trio/__main__.py +3 -0
- package/lib/site-packages/trio/_abc.py +714 -0
- package/lib/site-packages/trio/_channel.py +610 -0
- package/lib/site-packages/trio/_core/__init__.py +94 -0
- package/lib/site-packages/trio/_core/_asyncgens.py +243 -0
- package/lib/site-packages/trio/_core/_concat_tb.py +26 -0
- package/lib/site-packages/trio/_core/_entry_queue.py +223 -0
- package/lib/site-packages/trio/_core/_exceptions.py +169 -0
- package/lib/site-packages/trio/_core/_generated_instrumentation.py +50 -0
- package/lib/site-packages/trio/_core/_generated_io_epoll.py +98 -0
- package/lib/site-packages/trio/_core/_generated_io_kqueue.py +153 -0
- package/lib/site-packages/trio/_core/_generated_io_windows.py +204 -0
- package/lib/site-packages/trio/_core/_generated_run.py +269 -0
- package/lib/site-packages/trio/_core/_generated_windows_ffi.py +10 -0
- package/lib/site-packages/trio/_core/_instrumentation.py +117 -0
- package/lib/site-packages/trio/_core/_io_common.py +31 -0
- package/lib/site-packages/trio/_core/_io_epoll.py +385 -0
- package/lib/site-packages/trio/_core/_io_kqueue.py +292 -0
- package/lib/site-packages/trio/_core/_io_windows.py +1036 -0
- package/lib/site-packages/trio/_core/_ki.py +271 -0
- package/lib/site-packages/trio/_core/_local.py +104 -0
- package/lib/site-packages/trio/_core/_mock_clock.py +165 -0
- package/lib/site-packages/trio/_core/_parking_lot.py +317 -0
- package/lib/site-packages/trio/_core/_run.py +3148 -0
- package/lib/site-packages/trio/_core/_run_context.py +15 -0
- package/lib/site-packages/trio/_core/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_core/_tests/test_asyncgen.py +339 -0
- package/lib/site-packages/trio/_core/_tests/test_cancelled.py +222 -0
- package/lib/site-packages/trio/_core/_tests/test_exceptiongroup_gc.py +103 -0
- package/lib/site-packages/trio/_core/_tests/test_guest_mode.py +755 -0
- package/lib/site-packages/trio/_core/_tests/test_instrumentation.py +315 -0
- package/lib/site-packages/trio/_core/_tests/test_io.py +522 -0
- package/lib/site-packages/trio/_core/_tests/test_ki.py +703 -0
- package/lib/site-packages/trio/_core/_tests/test_local.py +118 -0
- package/lib/site-packages/trio/_core/_tests/test_mock_clock.py +193 -0
- package/lib/site-packages/trio/_core/_tests/test_parking_lot.py +389 -0
- package/lib/site-packages/trio/_core/_tests/test_run.py +3024 -0
- package/lib/site-packages/trio/_core/_tests/test_thread_cache.py +227 -0
- package/lib/site-packages/trio/_core/_tests/test_tutil.py +13 -0
- package/lib/site-packages/trio/_core/_tests/test_unbounded_queue.py +154 -0
- package/lib/site-packages/trio/_core/_tests/test_windows.py +305 -0
- package/lib/site-packages/trio/_core/_tests/tutil.py +117 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/nursery_start.py +79 -0
- package/lib/site-packages/trio/_core/_tests/type_tests/run.py +51 -0
- package/lib/site-packages/trio/_core/_thread_cache.py +317 -0
- package/lib/site-packages/trio/_core/_traps.py +318 -0
- package/lib/site-packages/trio/_core/_unbounded_queue.py +163 -0
- package/lib/site-packages/trio/_core/_wakeup_socketpair.py +75 -0
- package/lib/site-packages/trio/_core/_windows_cffi.py +313 -0
- package/lib/site-packages/trio/_deprecate.py +171 -0
- package/lib/site-packages/trio/_dtls.py +1380 -0
- package/lib/site-packages/trio/_file_io.py +513 -0
- package/lib/site-packages/trio/_highlevel_generic.py +125 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_listeners.py +251 -0
- package/lib/site-packages/trio/_highlevel_open_tcp_stream.py +397 -0
- package/lib/site-packages/trio/_highlevel_open_unix_stream.py +65 -0
- package/lib/site-packages/trio/_highlevel_serve_listeners.py +148 -0
- package/lib/site-packages/trio/_highlevel_socket.py +423 -0
- package/lib/site-packages/trio/_highlevel_ssl_helpers.py +180 -0
- package/lib/site-packages/trio/_path.py +289 -0
- package/lib/site-packages/trio/_repl.py +159 -0
- package/lib/site-packages/trio/_signals.py +185 -0
- package/lib/site-packages/trio/_socket.py +1326 -0
- package/lib/site-packages/trio/_ssl.py +964 -0
- package/lib/site-packages/trio/_subprocess.py +1178 -0
- package/lib/site-packages/trio/_subprocess_platform/__init__.py +123 -0
- package/lib/site-packages/trio/_subprocess_platform/kqueue.py +48 -0
- package/lib/site-packages/trio/_subprocess_platform/waitid.py +113 -0
- package/lib/site-packages/trio/_subprocess_platform/windows.py +11 -0
- package/lib/site-packages/trio/_sync.py +908 -0
- package/lib/site-packages/trio/_tests/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/astrill-codesigning-cert.cer +0 -0
- package/lib/site-packages/trio/_tests/check_type_completeness.py +247 -0
- package/lib/site-packages/trio/_tests/module_with_deprecations.py +22 -0
- package/lib/site-packages/trio/_tests/pytest_plugin.py +54 -0
- package/lib/site-packages/trio/_tests/test_abc.py +72 -0
- package/lib/site-packages/trio/_tests/test_channel.py +750 -0
- package/lib/site-packages/trio/_tests/test_contextvars.py +56 -0
- package/lib/site-packages/trio/_tests/test_deprecate.py +277 -0
- package/lib/site-packages/trio/_tests/test_deprecate_strict_exception_groups_false.py +64 -0
- package/lib/site-packages/trio/_tests/test_dtls.py +950 -0
- package/lib/site-packages/trio/_tests/test_exports.py +626 -0
- package/lib/site-packages/trio/_tests/test_fakenet.py +317 -0
- package/lib/site-packages/trio/_tests/test_file_io.py +269 -0
- package/lib/site-packages/trio/_tests/test_highlevel_generic.py +98 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_listeners.py +419 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_stream.py +693 -0
- package/lib/site-packages/trio/_tests/test_highlevel_open_unix_stream.py +86 -0
- package/lib/site-packages/trio/_tests/test_highlevel_serve_listeners.py +186 -0
- package/lib/site-packages/trio/_tests/test_highlevel_socket.py +336 -0
- package/lib/site-packages/trio/_tests/test_highlevel_ssl_helpers.py +169 -0
- package/lib/site-packages/trio/_tests/test_path.py +279 -0
- package/lib/site-packages/trio/_tests/test_repl.py +428 -0
- package/lib/site-packages/trio/_tests/test_scheduler_determinism.py +47 -0
- package/lib/site-packages/trio/_tests/test_signals.py +186 -0
- package/lib/site-packages/trio/_tests/test_socket.py +1253 -0
- package/lib/site-packages/trio/_tests/test_ssl.py +1371 -0
- package/lib/site-packages/trio/_tests/test_subprocess.py +767 -0
- package/lib/site-packages/trio/_tests/test_sync.py +735 -0
- package/lib/site-packages/trio/_tests/test_testing.py +682 -0
- package/lib/site-packages/trio/_tests/test_testing_raisesgroup.py +1128 -0
- package/lib/site-packages/trio/_tests/test_threads.py +1173 -0
- package/lib/site-packages/trio/_tests/test_timeouts.py +281 -0
- package/lib/site-packages/trio/_tests/test_tracing.py +88 -0
- package/lib/site-packages/trio/_tests/test_trio.py +8 -0
- package/lib/site-packages/trio/_tests/test_unix_pipes.py +288 -0
- package/lib/site-packages/trio/_tests/test_util.py +349 -0
- package/lib/site-packages/trio/_tests/test_wait_for_object.py +225 -0
- package/lib/site-packages/trio/_tests/test_windows_pipes.py +112 -0
- package/lib/site-packages/trio/_tests/tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tests/tools/test_gen_exports.py +179 -0
- package/lib/site-packages/trio/_tests/tools/test_mypy_annotate.py +140 -0
- package/lib/site-packages/trio/_tests/tools/test_sync_requirements.py +80 -0
- package/lib/site-packages/trio/_tests/type_tests/check_wraps.py +9 -0
- package/lib/site-packages/trio/_tests/type_tests/open_memory_channel.py +4 -0
- package/lib/site-packages/trio/_tests/type_tests/path.py +140 -0
- package/lib/site-packages/trio/_tests/type_tests/subprocesses.py +23 -0
- package/lib/site-packages/trio/_tests/type_tests/task_status.py +29 -0
- package/lib/site-packages/trio/_threads.py +610 -0
- package/lib/site-packages/trio/_timeouts.py +197 -0
- package/lib/site-packages/trio/_tools/__init__.py +0 -0
- package/lib/site-packages/trio/_tools/gen_exports.py +401 -0
- package/lib/site-packages/trio/_tools/mypy_annotate.py +126 -0
- package/lib/site-packages/trio/_tools/sync_requirements.py +98 -0
- package/lib/site-packages/trio/_tools/windows_ffi_build.py +220 -0
- package/lib/site-packages/trio/_unix_pipes.py +197 -0
- package/lib/site-packages/trio/_util.py +385 -0
- package/lib/site-packages/trio/_version.py +3 -0
- package/lib/site-packages/trio/_wait_for_object.py +67 -0
- package/lib/site-packages/trio/_windows_pipes.py +144 -0
- package/lib/site-packages/trio/abc.py +23 -0
- package/lib/site-packages/trio/from_thread.py +13 -0
- package/lib/site-packages/trio/lowlevel.py +95 -0
- package/lib/site-packages/trio/py.typed +0 -0
- package/lib/site-packages/trio/socket.py +602 -0
- package/lib/site-packages/trio/testing/__init__.py +58 -0
- package/lib/site-packages/trio/testing/_check_streams.py +570 -0
- package/lib/site-packages/trio/testing/_checkpoints.py +69 -0
- package/lib/site-packages/trio/testing/_fake_net.py +584 -0
- package/lib/site-packages/trio/testing/_memory_streams.py +633 -0
- package/lib/site-packages/trio/testing/_network.py +36 -0
- package/lib/site-packages/trio/testing/_raises_group.py +1015 -0
- package/lib/site-packages/trio/testing/_sequencer.py +87 -0
- package/lib/site-packages/trio/testing/_trio_test.py +50 -0
- package/lib/site-packages/trio/to_thread.py +4 -0
- package/lib/site-packages/trio-0.33.0.dist-info/INSTALLER +1 -0
- package/lib/site-packages/trio-0.33.0.dist-info/METADATA +186 -0
- package/lib/site-packages/trio-0.33.0.dist-info/RECORD +156 -0
- package/lib/site-packages/trio-0.33.0.dist-info/REQUESTED +0 -0
- package/lib/site-packages/trio-0.33.0.dist-info/WHEEL +5 -0
- package/lib/site-packages/trio-0.33.0.dist-info/entry_points.txt +2 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE +3 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.APACHE2 +202 -0
- package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.MIT +22 -0
- package/lib/site-packages/trio-0.33.0.dist-info/top_level.txt +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
import queue as stdlib_queue
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import weakref
|
|
10
|
+
from functools import partial
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
NoReturn,
|
|
14
|
+
TypeVar,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import sniffio
|
|
19
|
+
|
|
20
|
+
from .. import (
|
|
21
|
+
CancelScope,
|
|
22
|
+
CapacityLimiter,
|
|
23
|
+
Event,
|
|
24
|
+
_core,
|
|
25
|
+
fail_after,
|
|
26
|
+
move_on_after,
|
|
27
|
+
sleep,
|
|
28
|
+
sleep_forever,
|
|
29
|
+
)
|
|
30
|
+
from .._core._tests.test_ki import ki_self
|
|
31
|
+
from .._core._tests.tutil import slow
|
|
32
|
+
from .._threads import (
|
|
33
|
+
active_thread_count,
|
|
34
|
+
current_default_thread_limiter,
|
|
35
|
+
from_thread_check_cancelled,
|
|
36
|
+
from_thread_run,
|
|
37
|
+
from_thread_run_sync,
|
|
38
|
+
to_thread_run_sync,
|
|
39
|
+
wait_all_threads_completed,
|
|
40
|
+
)
|
|
41
|
+
from ..testing import wait_all_tasks_blocked
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
45
|
+
|
|
46
|
+
from outcome import Outcome
|
|
47
|
+
|
|
48
|
+
from ..lowlevel import Task
|
|
49
|
+
|
|
50
|
+
RecordType = list[tuple[str, threading.Thread | type[BaseException]]]
|
|
51
|
+
T = TypeVar("T")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def test_do_in_trio_thread() -> None:
|
|
55
|
+
trio_thread = threading.current_thread()
|
|
56
|
+
|
|
57
|
+
async def check_case( # type: ignore[explicit-any]
|
|
58
|
+
do_in_trio_thread: Callable[..., threading.Thread],
|
|
59
|
+
fn: Callable[..., T | Awaitable[T]],
|
|
60
|
+
expected: tuple[str, T],
|
|
61
|
+
trio_token: _core.TrioToken | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
record: RecordType = []
|
|
64
|
+
|
|
65
|
+
def threadfn() -> None:
|
|
66
|
+
try:
|
|
67
|
+
record.append(("start", threading.current_thread()))
|
|
68
|
+
x = do_in_trio_thread(fn, record, trio_token=trio_token)
|
|
69
|
+
record.append(("got", x))
|
|
70
|
+
except BaseException as exc:
|
|
71
|
+
print(exc)
|
|
72
|
+
record.append(("error", type(exc)))
|
|
73
|
+
|
|
74
|
+
child_thread = threading.Thread(target=threadfn, daemon=True)
|
|
75
|
+
child_thread.start()
|
|
76
|
+
while child_thread.is_alive():
|
|
77
|
+
print("yawn")
|
|
78
|
+
await sleep(0.01)
|
|
79
|
+
assert record == [("start", child_thread), ("f", trio_thread), expected]
|
|
80
|
+
|
|
81
|
+
token = _core.current_trio_token()
|
|
82
|
+
|
|
83
|
+
def f1(record: RecordType) -> int:
|
|
84
|
+
assert not _core.currently_ki_protected()
|
|
85
|
+
record.append(("f", threading.current_thread()))
|
|
86
|
+
return 2
|
|
87
|
+
|
|
88
|
+
await check_case(from_thread_run_sync, f1, ("got", 2), trio_token=token)
|
|
89
|
+
|
|
90
|
+
def f2(record: RecordType) -> NoReturn:
|
|
91
|
+
assert not _core.currently_ki_protected()
|
|
92
|
+
record.append(("f", threading.current_thread()))
|
|
93
|
+
raise ValueError
|
|
94
|
+
|
|
95
|
+
await check_case(from_thread_run_sync, f2, ("error", ValueError), trio_token=token)
|
|
96
|
+
|
|
97
|
+
async def f3(record: RecordType) -> int:
|
|
98
|
+
assert not _core.currently_ki_protected()
|
|
99
|
+
await _core.checkpoint()
|
|
100
|
+
record.append(("f", threading.current_thread()))
|
|
101
|
+
return 3
|
|
102
|
+
|
|
103
|
+
await check_case(from_thread_run, f3, ("got", 3), trio_token=token)
|
|
104
|
+
|
|
105
|
+
async def f4(record: RecordType) -> NoReturn:
|
|
106
|
+
assert not _core.currently_ki_protected()
|
|
107
|
+
await _core.checkpoint()
|
|
108
|
+
record.append(("f", threading.current_thread()))
|
|
109
|
+
raise KeyError
|
|
110
|
+
|
|
111
|
+
await check_case(from_thread_run, f4, ("error", KeyError), trio_token=token)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def test_do_in_trio_thread_from_trio_thread() -> None:
|
|
115
|
+
with pytest.raises(RuntimeError):
|
|
116
|
+
from_thread_run_sync(lambda: None) # pragma: no branch
|
|
117
|
+
|
|
118
|
+
async def foo() -> None: # pragma: no cover
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
with pytest.raises(RuntimeError):
|
|
122
|
+
from_thread_run(foo)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_run_in_trio_thread_ki() -> None:
|
|
126
|
+
# if we get a control-C during a run_in_trio_thread, then it propagates
|
|
127
|
+
# back to the caller (slick!)
|
|
128
|
+
record = set()
|
|
129
|
+
|
|
130
|
+
async def check_run_in_trio_thread() -> None:
|
|
131
|
+
token = _core.current_trio_token()
|
|
132
|
+
|
|
133
|
+
def trio_thread_fn() -> None:
|
|
134
|
+
print("in Trio thread")
|
|
135
|
+
assert not _core.currently_ki_protected()
|
|
136
|
+
print("ki_self")
|
|
137
|
+
try:
|
|
138
|
+
ki_self()
|
|
139
|
+
finally:
|
|
140
|
+
import sys
|
|
141
|
+
|
|
142
|
+
print("finally", sys.exc_info())
|
|
143
|
+
|
|
144
|
+
async def trio_thread_afn() -> None:
|
|
145
|
+
trio_thread_fn()
|
|
146
|
+
|
|
147
|
+
def external_thread_fn() -> None:
|
|
148
|
+
try:
|
|
149
|
+
print("running")
|
|
150
|
+
from_thread_run_sync(trio_thread_fn, trio_token=token)
|
|
151
|
+
except KeyboardInterrupt:
|
|
152
|
+
print("ok1")
|
|
153
|
+
record.add("ok1")
|
|
154
|
+
try:
|
|
155
|
+
from_thread_run(trio_thread_afn, trio_token=token)
|
|
156
|
+
except KeyboardInterrupt:
|
|
157
|
+
print("ok2")
|
|
158
|
+
record.add("ok2")
|
|
159
|
+
|
|
160
|
+
thread = threading.Thread(target=external_thread_fn)
|
|
161
|
+
thread.start()
|
|
162
|
+
print("waiting")
|
|
163
|
+
while thread.is_alive(): # noqa: ASYNC110
|
|
164
|
+
await sleep(0.01) # Fine to poll in tests.
|
|
165
|
+
print("waited, joining")
|
|
166
|
+
thread.join()
|
|
167
|
+
print("done")
|
|
168
|
+
|
|
169
|
+
_core.run(check_run_in_trio_thread)
|
|
170
|
+
assert record == {"ok1", "ok2"}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_await_in_trio_thread_while_main_exits() -> None:
|
|
174
|
+
record = []
|
|
175
|
+
ev = Event()
|
|
176
|
+
|
|
177
|
+
async def trio_fn() -> None:
|
|
178
|
+
record.append("sleeping")
|
|
179
|
+
ev.set()
|
|
180
|
+
await _core.wait_task_rescheduled(lambda _: _core.Abort.SUCCEEDED)
|
|
181
|
+
|
|
182
|
+
def thread_fn(token: _core.TrioToken) -> None:
|
|
183
|
+
try:
|
|
184
|
+
from_thread_run(trio_fn, trio_token=token)
|
|
185
|
+
except _core.Cancelled:
|
|
186
|
+
record.append("cancelled")
|
|
187
|
+
|
|
188
|
+
async def main() -> threading.Thread:
|
|
189
|
+
token = _core.current_trio_token()
|
|
190
|
+
thread = threading.Thread(target=thread_fn, args=(token,))
|
|
191
|
+
thread.start()
|
|
192
|
+
await ev.wait()
|
|
193
|
+
assert record == ["sleeping"]
|
|
194
|
+
return thread
|
|
195
|
+
|
|
196
|
+
thread = _core.run(main)
|
|
197
|
+
thread.join()
|
|
198
|
+
assert record == ["sleeping", "cancelled"]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def test_named_thread() -> None:
|
|
202
|
+
ending = " from trio._tests.test_threads.test_named_thread"
|
|
203
|
+
|
|
204
|
+
def inner(name: str = "inner" + ending) -> threading.Thread:
|
|
205
|
+
assert threading.current_thread().name == name
|
|
206
|
+
return threading.current_thread()
|
|
207
|
+
|
|
208
|
+
def f(name: str) -> Callable[[], threading.Thread]:
|
|
209
|
+
return partial(inner, name)
|
|
210
|
+
|
|
211
|
+
# test defaults
|
|
212
|
+
await to_thread_run_sync(inner)
|
|
213
|
+
await to_thread_run_sync(inner, thread_name=None)
|
|
214
|
+
|
|
215
|
+
# functools.partial doesn't have __name__, so defaults to None
|
|
216
|
+
await to_thread_run_sync(f("None" + ending))
|
|
217
|
+
|
|
218
|
+
# test that you can set a custom name, and that it's reset afterwards
|
|
219
|
+
async def test_thread_name(name: str) -> None:
|
|
220
|
+
thread = await to_thread_run_sync(f(name), thread_name=name)
|
|
221
|
+
assert re.match(r"Trio thread [0-9]*", thread.name)
|
|
222
|
+
|
|
223
|
+
await test_thread_name("")
|
|
224
|
+
await test_thread_name("fobiedoo")
|
|
225
|
+
await test_thread_name("name_longer_than_15_characters")
|
|
226
|
+
|
|
227
|
+
await test_thread_name("💙")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _get_thread_name(ident: int | None = None) -> str | None:
|
|
231
|
+
import ctypes
|
|
232
|
+
import ctypes.util
|
|
233
|
+
|
|
234
|
+
libpthread_path = ctypes.util.find_library("pthread")
|
|
235
|
+
if not libpthread_path:
|
|
236
|
+
# musl includes pthread functions directly in libc.so
|
|
237
|
+
# (but note that find_library("c") does not work on musl,
|
|
238
|
+
# see: https://github.com/python/cpython/issues/65821)
|
|
239
|
+
# so try that library instead
|
|
240
|
+
# if it doesn't exist, CDLL() will fail below
|
|
241
|
+
libpthread_path = "libc.so"
|
|
242
|
+
try:
|
|
243
|
+
libpthread = ctypes.CDLL(libpthread_path)
|
|
244
|
+
except Exception:
|
|
245
|
+
print(f"no pthread on {sys.platform}")
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
pthread_getname_np = getattr(libpthread, "pthread_getname_np", None)
|
|
249
|
+
|
|
250
|
+
# this should never fail on any platforms afaik
|
|
251
|
+
assert pthread_getname_np
|
|
252
|
+
|
|
253
|
+
# thankfully getname signature doesn't differ between platforms
|
|
254
|
+
pthread_getname_np.argtypes = [
|
|
255
|
+
ctypes.c_void_p,
|
|
256
|
+
ctypes.c_char_p,
|
|
257
|
+
ctypes.c_size_t,
|
|
258
|
+
]
|
|
259
|
+
pthread_getname_np.restype = ctypes.c_int
|
|
260
|
+
|
|
261
|
+
name_buffer = ctypes.create_string_buffer(b"", size=16)
|
|
262
|
+
if ident is None:
|
|
263
|
+
ident = threading.get_ident()
|
|
264
|
+
assert pthread_getname_np(ident, name_buffer, 16) == 0
|
|
265
|
+
try:
|
|
266
|
+
return name_buffer.value.decode()
|
|
267
|
+
except UnicodeDecodeError as e: # pragma: no cover
|
|
268
|
+
# used for debugging when testing via CI
|
|
269
|
+
pytest.fail(f"value: {name_buffer.value!r}, exception: {e}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# test os thread naming
|
|
273
|
+
# this depends on pthread being available, which is the case on 99.9% of linux machines
|
|
274
|
+
# and most mac machines. So unless the platform is linux it will just skip
|
|
275
|
+
# in case it fails to fetch the os thread name.
|
|
276
|
+
async def test_named_thread_os() -> None:
|
|
277
|
+
def inner(name: str) -> threading.Thread:
|
|
278
|
+
os_thread_name = _get_thread_name()
|
|
279
|
+
if os_thread_name is None and sys.platform != "linux":
|
|
280
|
+
pytest.skip(f"no pthread OS support on {sys.platform}")
|
|
281
|
+
else:
|
|
282
|
+
assert os_thread_name == name[:15]
|
|
283
|
+
|
|
284
|
+
return threading.current_thread()
|
|
285
|
+
|
|
286
|
+
def f(name: str) -> Callable[[], threading.Thread]:
|
|
287
|
+
return partial(inner, name)
|
|
288
|
+
|
|
289
|
+
# test defaults
|
|
290
|
+
default = "None from trio._tests.test_threads.test_named_thread"
|
|
291
|
+
await to_thread_run_sync(f(default))
|
|
292
|
+
await to_thread_run_sync(f(default), thread_name=None)
|
|
293
|
+
|
|
294
|
+
# test that you can set a custom name, and that it's reset afterwards
|
|
295
|
+
async def test_thread_name(name: str, expected: str | None = None) -> None:
|
|
296
|
+
if expected is None:
|
|
297
|
+
expected = name
|
|
298
|
+
thread = await to_thread_run_sync(f(expected), thread_name=name)
|
|
299
|
+
|
|
300
|
+
os_thread_name = _get_thread_name(thread.ident)
|
|
301
|
+
assert os_thread_name is not None, "should skip earlier if this is the case"
|
|
302
|
+
assert re.match(r"Trio thread [0-9]*", os_thread_name)
|
|
303
|
+
|
|
304
|
+
await test_thread_name("")
|
|
305
|
+
await test_thread_name("fobiedoo")
|
|
306
|
+
await test_thread_name("name_longer_than_15_characters")
|
|
307
|
+
|
|
308
|
+
await test_thread_name("💙", expected="?")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_has_pthread_setname_np() -> None:
|
|
312
|
+
from trio._core._thread_cache import get_os_thread_name_func
|
|
313
|
+
|
|
314
|
+
k = get_os_thread_name_func()
|
|
315
|
+
if k is None:
|
|
316
|
+
assert sys.platform != "linux"
|
|
317
|
+
pytest.skip(f"no pthread_setname_np on {sys.platform}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def test_run_in_worker_thread() -> None:
|
|
321
|
+
trio_thread = threading.current_thread()
|
|
322
|
+
|
|
323
|
+
def f(x: T) -> tuple[T, threading.Thread]:
|
|
324
|
+
return (x, threading.current_thread())
|
|
325
|
+
|
|
326
|
+
x, child_thread = await to_thread_run_sync(f, 1)
|
|
327
|
+
assert x == 1
|
|
328
|
+
assert child_thread != trio_thread
|
|
329
|
+
|
|
330
|
+
def g() -> NoReturn:
|
|
331
|
+
raise ValueError(threading.current_thread())
|
|
332
|
+
|
|
333
|
+
with pytest.raises(
|
|
334
|
+
ValueError,
|
|
335
|
+
match=r"^<Thread\(Trio thread \d+, started daemon \d+\)>$",
|
|
336
|
+
) as excinfo:
|
|
337
|
+
await to_thread_run_sync(g)
|
|
338
|
+
print(excinfo.value.args)
|
|
339
|
+
assert excinfo.value.args[0] != trio_thread
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async def test_run_in_worker_thread_cancellation() -> None:
|
|
343
|
+
register: list[str | None] = [None]
|
|
344
|
+
|
|
345
|
+
def f(q: stdlib_queue.Queue[None]) -> None:
|
|
346
|
+
# Make the thread block for a controlled amount of time
|
|
347
|
+
register[0] = "blocking"
|
|
348
|
+
q.get()
|
|
349
|
+
register[0] = "finished"
|
|
350
|
+
|
|
351
|
+
async def child(q: stdlib_queue.Queue[None], abandon_on_cancel: bool) -> None:
|
|
352
|
+
record.append("start")
|
|
353
|
+
try:
|
|
354
|
+
return await to_thread_run_sync(f, q, abandon_on_cancel=abandon_on_cancel)
|
|
355
|
+
finally:
|
|
356
|
+
record.append("exit")
|
|
357
|
+
|
|
358
|
+
record: list[str] = []
|
|
359
|
+
q: stdlib_queue.Queue[None] = stdlib_queue.Queue()
|
|
360
|
+
async with _core.open_nursery() as nursery:
|
|
361
|
+
nursery.start_soon(child, q, True)
|
|
362
|
+
# Give it a chance to get started. (This is important because
|
|
363
|
+
# to_thread_run_sync does a checkpoint_if_cancelled before
|
|
364
|
+
# blocking on the thread, and we don't want to trigger this.)
|
|
365
|
+
await wait_all_tasks_blocked()
|
|
366
|
+
assert record == ["start"]
|
|
367
|
+
# Then cancel it.
|
|
368
|
+
nursery.cancel_scope.cancel()
|
|
369
|
+
# The task exited, but the thread didn't:
|
|
370
|
+
assert register[0] != "finished"
|
|
371
|
+
# Put the thread out of its misery:
|
|
372
|
+
q.put(None)
|
|
373
|
+
while register[0] != "finished":
|
|
374
|
+
time.sleep(0.01) # noqa: ASYNC251 # Need to wait for OS thread
|
|
375
|
+
|
|
376
|
+
# This one can't be cancelled
|
|
377
|
+
record = []
|
|
378
|
+
register[0] = None
|
|
379
|
+
async with _core.open_nursery() as nursery:
|
|
380
|
+
nursery.start_soon(child, q, False)
|
|
381
|
+
await wait_all_tasks_blocked()
|
|
382
|
+
nursery.cancel_scope.cancel()
|
|
383
|
+
with _core.CancelScope(shield=True):
|
|
384
|
+
for _ in range(10):
|
|
385
|
+
await _core.checkpoint()
|
|
386
|
+
# It's still running
|
|
387
|
+
assert record == ["start"]
|
|
388
|
+
q.put(None)
|
|
389
|
+
# Now it exits
|
|
390
|
+
|
|
391
|
+
# But if we cancel *before* it enters, the entry is itself a cancellation
|
|
392
|
+
# point
|
|
393
|
+
with _core.CancelScope() as scope:
|
|
394
|
+
scope.cancel()
|
|
395
|
+
await child(q, False)
|
|
396
|
+
assert scope.cancelled_caught
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# Make sure that if trio.run exits, and then the thread finishes, then that's
|
|
400
|
+
# handled gracefully. (Requires that the thread result machinery be prepared
|
|
401
|
+
# for call_soon to raise RunFinishedError.)
|
|
402
|
+
def test_run_in_worker_thread_abandoned(
|
|
403
|
+
capfd: pytest.CaptureFixture[str],
|
|
404
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
405
|
+
) -> None:
|
|
406
|
+
monkeypatch.setattr(_core._thread_cache, "IDLE_TIMEOUT", 0.01)
|
|
407
|
+
|
|
408
|
+
q1: stdlib_queue.Queue[None] = stdlib_queue.Queue()
|
|
409
|
+
q2: stdlib_queue.Queue[threading.Thread] = stdlib_queue.Queue()
|
|
410
|
+
|
|
411
|
+
def thread_fn() -> None:
|
|
412
|
+
q1.get()
|
|
413
|
+
q2.put(threading.current_thread())
|
|
414
|
+
|
|
415
|
+
async def main() -> None:
|
|
416
|
+
async def child() -> None:
|
|
417
|
+
await to_thread_run_sync(thread_fn, abandon_on_cancel=True)
|
|
418
|
+
|
|
419
|
+
async with _core.open_nursery() as nursery:
|
|
420
|
+
nursery.start_soon(child)
|
|
421
|
+
await wait_all_tasks_blocked()
|
|
422
|
+
nursery.cancel_scope.cancel()
|
|
423
|
+
|
|
424
|
+
_core.run(main)
|
|
425
|
+
|
|
426
|
+
q1.put(None)
|
|
427
|
+
# This makes sure:
|
|
428
|
+
# - the thread actually ran
|
|
429
|
+
# - that thread has finished before we check for its output
|
|
430
|
+
thread = q2.get()
|
|
431
|
+
while thread.is_alive():
|
|
432
|
+
time.sleep(0.01) # pragma: no cover
|
|
433
|
+
|
|
434
|
+
# Make sure we don't have a "Exception in thread ..." dump to the console:
|
|
435
|
+
out, err = capfd.readouterr()
|
|
436
|
+
assert "Exception in thread" not in out
|
|
437
|
+
assert "Exception in thread" not in err
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@pytest.mark.parametrize("MAX", [3, 5, 10])
|
|
441
|
+
@pytest.mark.parametrize("cancel", [False, True])
|
|
442
|
+
@pytest.mark.parametrize("use_default_limiter", [False, True])
|
|
443
|
+
async def test_run_in_worker_thread_limiter(
|
|
444
|
+
MAX: int,
|
|
445
|
+
cancel: bool,
|
|
446
|
+
use_default_limiter: bool,
|
|
447
|
+
) -> None:
|
|
448
|
+
# This test is a bit tricky. The goal is to make sure that if we set
|
|
449
|
+
# limiter=CapacityLimiter(MAX), then in fact only MAX threads are ever
|
|
450
|
+
# running at a time, even if there are more concurrent calls to
|
|
451
|
+
# to_thread_run_sync, and even if some of those are cancelled. And
|
|
452
|
+
# also to make sure that the default limiter actually limits.
|
|
453
|
+
COUNT = 2 * MAX
|
|
454
|
+
gate = threading.Event()
|
|
455
|
+
lock = threading.Lock()
|
|
456
|
+
if use_default_limiter:
|
|
457
|
+
c = current_default_thread_limiter()
|
|
458
|
+
orig_total_tokens = c.total_tokens
|
|
459
|
+
c.total_tokens = MAX
|
|
460
|
+
limiter_arg = None
|
|
461
|
+
else:
|
|
462
|
+
c = CapacityLimiter(MAX)
|
|
463
|
+
orig_total_tokens = MAX
|
|
464
|
+
limiter_arg = c
|
|
465
|
+
try:
|
|
466
|
+
# We used to use regular variables and 'nonlocal' here, but it turns
|
|
467
|
+
# out that it's not safe to assign to closed-over variables that are
|
|
468
|
+
# visible in multiple threads, at least as of CPython 3.10 and PyPy
|
|
469
|
+
# 7.3:
|
|
470
|
+
#
|
|
471
|
+
# https://bugs.python.org/issue30744
|
|
472
|
+
# https://bitbucket.org/pypy/pypy/issues/2591/
|
|
473
|
+
#
|
|
474
|
+
# Mutating them in-place is OK though (as long as you use proper
|
|
475
|
+
# locking etc.).
|
|
476
|
+
class state:
|
|
477
|
+
ran: int
|
|
478
|
+
high_water: int
|
|
479
|
+
running: int
|
|
480
|
+
parked: int
|
|
481
|
+
|
|
482
|
+
state.ran = 0
|
|
483
|
+
state.high_water = 0
|
|
484
|
+
state.running = 0
|
|
485
|
+
state.parked = 0
|
|
486
|
+
|
|
487
|
+
token = _core.current_trio_token()
|
|
488
|
+
|
|
489
|
+
def thread_fn(cancel_scope: CancelScope) -> None:
|
|
490
|
+
print("thread_fn start")
|
|
491
|
+
from_thread_run_sync(cancel_scope.cancel, trio_token=token)
|
|
492
|
+
with lock:
|
|
493
|
+
state.ran += 1
|
|
494
|
+
state.running += 1
|
|
495
|
+
state.high_water = max(state.high_water, state.running)
|
|
496
|
+
# The Trio thread below watches this value and uses it as a
|
|
497
|
+
# signal that all the stats calculations have finished.
|
|
498
|
+
state.parked += 1
|
|
499
|
+
gate.wait()
|
|
500
|
+
with lock:
|
|
501
|
+
state.parked -= 1
|
|
502
|
+
state.running -= 1
|
|
503
|
+
print("thread_fn exiting")
|
|
504
|
+
|
|
505
|
+
async def run_thread(event: Event) -> None:
|
|
506
|
+
with _core.CancelScope() as cancel_scope:
|
|
507
|
+
await to_thread_run_sync(
|
|
508
|
+
thread_fn,
|
|
509
|
+
cancel_scope,
|
|
510
|
+
abandon_on_cancel=cancel,
|
|
511
|
+
limiter=limiter_arg,
|
|
512
|
+
)
|
|
513
|
+
print("run_thread finished, cancelled:", cancel_scope.cancelled_caught)
|
|
514
|
+
event.set()
|
|
515
|
+
|
|
516
|
+
async with _core.open_nursery() as nursery:
|
|
517
|
+
print("spawning")
|
|
518
|
+
events = []
|
|
519
|
+
for _ in range(COUNT):
|
|
520
|
+
events.append(Event())
|
|
521
|
+
nursery.start_soon(run_thread, events[-1])
|
|
522
|
+
await wait_all_tasks_blocked()
|
|
523
|
+
# In the cancel case, we in particular want to make sure that the
|
|
524
|
+
# cancelled tasks don't release the semaphore. So let's wait until
|
|
525
|
+
# at least one of them has exited, and that everything has had a
|
|
526
|
+
# chance to settle down from this, before we check that everyone
|
|
527
|
+
# who's supposed to be waiting is waiting:
|
|
528
|
+
if cancel:
|
|
529
|
+
print("waiting for first cancellation to clear")
|
|
530
|
+
await events[0].wait()
|
|
531
|
+
await wait_all_tasks_blocked()
|
|
532
|
+
# Then wait until the first MAX threads are parked in gate.wait(),
|
|
533
|
+
# and the next MAX threads are parked on the semaphore, to make
|
|
534
|
+
# sure no-one is sneaking past, and to make sure the high_water
|
|
535
|
+
# check below won't fail due to scheduling issues. (It could still
|
|
536
|
+
# fail if too many threads are let through here.)
|
|
537
|
+
while ( # noqa: ASYNC110
|
|
538
|
+
state.parked != MAX or c.statistics().tasks_waiting != MAX
|
|
539
|
+
):
|
|
540
|
+
await sleep(0.01) # pragma: no cover
|
|
541
|
+
# Then release the threads
|
|
542
|
+
gate.set()
|
|
543
|
+
|
|
544
|
+
assert state.high_water == MAX
|
|
545
|
+
|
|
546
|
+
if cancel:
|
|
547
|
+
# Some threads might still be running; need to wait to them to
|
|
548
|
+
# finish before checking that all threads ran. We can do this
|
|
549
|
+
# using the CapacityLimiter.
|
|
550
|
+
while c.borrowed_tokens > 0: # noqa: ASYNC110
|
|
551
|
+
await sleep(0.01) # pragma: no cover
|
|
552
|
+
|
|
553
|
+
assert state.ran == COUNT
|
|
554
|
+
assert state.running == 0
|
|
555
|
+
finally:
|
|
556
|
+
c.total_tokens = orig_total_tokens
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
async def test_run_in_worker_thread_custom_limiter() -> None:
|
|
560
|
+
# Basically just checking that we only call acquire_on_behalf_of and
|
|
561
|
+
# release_on_behalf_of, since that's part of our documented API.
|
|
562
|
+
record = []
|
|
563
|
+
|
|
564
|
+
class CustomLimiter:
|
|
565
|
+
async def acquire_on_behalf_of(self, borrower: Task) -> None:
|
|
566
|
+
record.append("acquire")
|
|
567
|
+
self._borrower = borrower
|
|
568
|
+
|
|
569
|
+
def release_on_behalf_of(self, borrower: Task) -> None:
|
|
570
|
+
record.append("release")
|
|
571
|
+
assert borrower == self._borrower
|
|
572
|
+
|
|
573
|
+
# TODO: should CapacityLimiter have an abc or protocol so users can modify it?
|
|
574
|
+
# because currently it's `final` so writing code like this is not allowed.
|
|
575
|
+
await to_thread_run_sync(lambda: None, limiter=CustomLimiter()) # type: ignore[arg-type]
|
|
576
|
+
assert record == ["acquire", "release"]
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
async def test_run_in_worker_thread_limiter_error() -> None:
|
|
580
|
+
record = []
|
|
581
|
+
|
|
582
|
+
class BadCapacityLimiter:
|
|
583
|
+
async def acquire_on_behalf_of(self, borrower: Task) -> None:
|
|
584
|
+
record.append("acquire")
|
|
585
|
+
|
|
586
|
+
def release_on_behalf_of(self, borrower: Task) -> NoReturn:
|
|
587
|
+
record.append("release")
|
|
588
|
+
raise ValueError("release on behalf")
|
|
589
|
+
|
|
590
|
+
bs = BadCapacityLimiter()
|
|
591
|
+
|
|
592
|
+
with pytest.raises(ValueError, match=r"^release on behalf$") as excinfo:
|
|
593
|
+
await to_thread_run_sync(lambda: None, limiter=bs) # type: ignore[arg-type]
|
|
594
|
+
assert excinfo.value.__context__ is None
|
|
595
|
+
assert record == ["acquire", "release"]
|
|
596
|
+
record = []
|
|
597
|
+
|
|
598
|
+
# If the original function raised an error, then the semaphore error
|
|
599
|
+
# chains with it
|
|
600
|
+
d: dict[str, object] = {}
|
|
601
|
+
with pytest.raises(ValueError, match=r"^release on behalf$") as excinfo:
|
|
602
|
+
await to_thread_run_sync(lambda: d["x"], limiter=bs) # type: ignore[arg-type]
|
|
603
|
+
assert isinstance(excinfo.value.__context__, KeyError)
|
|
604
|
+
assert record == ["acquire", "release"]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def test_run_in_worker_thread_fail_to_spawn(
|
|
608
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
609
|
+
) -> None:
|
|
610
|
+
# Test the unlikely but possible case where trying to spawn a thread fails
|
|
611
|
+
def bad_start(self: object, *args: object) -> NoReturn:
|
|
612
|
+
raise RuntimeError("the engines canna take it captain")
|
|
613
|
+
|
|
614
|
+
monkeypatch.setattr(_core._thread_cache.ThreadCache, "start_thread_soon", bad_start)
|
|
615
|
+
|
|
616
|
+
limiter = current_default_thread_limiter()
|
|
617
|
+
assert limiter.borrowed_tokens == 0
|
|
618
|
+
|
|
619
|
+
# We get an appropriate error, and the limiter is cleanly released
|
|
620
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
621
|
+
await to_thread_run_sync(lambda: None) # pragma: no cover
|
|
622
|
+
assert "engines" in str(excinfo.value)
|
|
623
|
+
|
|
624
|
+
assert limiter.borrowed_tokens == 0
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
async def test_trio_to_thread_run_sync_token() -> None:
|
|
628
|
+
# Test that to_thread_run_sync automatically injects the current trio token
|
|
629
|
+
# into a spawned thread
|
|
630
|
+
def thread_fn() -> _core.TrioToken:
|
|
631
|
+
callee_token = from_thread_run_sync(_core.current_trio_token)
|
|
632
|
+
return callee_token
|
|
633
|
+
|
|
634
|
+
caller_token = _core.current_trio_token()
|
|
635
|
+
callee_token = await to_thread_run_sync(thread_fn)
|
|
636
|
+
assert callee_token == caller_token
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
async def test_trio_to_thread_run_sync_expected_error() -> None:
|
|
640
|
+
# Test correct error when passed async function
|
|
641
|
+
async def async_fn() -> None: # pragma: no cover
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
with pytest.raises(TypeError, match="expected a sync function"):
|
|
645
|
+
await to_thread_run_sync(async_fn) # type: ignore[unused-coroutine]
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
trio_test_contextvar: contextvars.ContextVar[str] = contextvars.ContextVar(
|
|
649
|
+
"trio_test_contextvar",
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
async def test_trio_to_thread_run_sync_contextvars() -> None:
|
|
654
|
+
trio_thread = threading.current_thread()
|
|
655
|
+
trio_test_contextvar.set("main")
|
|
656
|
+
|
|
657
|
+
def f() -> tuple[str, threading.Thread]:
|
|
658
|
+
value = trio_test_contextvar.get()
|
|
659
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
660
|
+
sniffio.current_async_library()
|
|
661
|
+
return (value, threading.current_thread())
|
|
662
|
+
|
|
663
|
+
value, child_thread = await to_thread_run_sync(f)
|
|
664
|
+
assert value == "main"
|
|
665
|
+
assert child_thread != trio_thread
|
|
666
|
+
|
|
667
|
+
def g() -> tuple[str, str, threading.Thread]:
|
|
668
|
+
parent_value = trio_test_contextvar.get()
|
|
669
|
+
trio_test_contextvar.set("worker")
|
|
670
|
+
inner_value = trio_test_contextvar.get()
|
|
671
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
672
|
+
sniffio.current_async_library()
|
|
673
|
+
return (
|
|
674
|
+
parent_value,
|
|
675
|
+
inner_value,
|
|
676
|
+
threading.current_thread(),
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
parent_value, inner_value, child_thread = await to_thread_run_sync(g)
|
|
680
|
+
current_value = trio_test_contextvar.get()
|
|
681
|
+
assert parent_value == "main"
|
|
682
|
+
assert inner_value == "worker"
|
|
683
|
+
assert current_value == "main", (
|
|
684
|
+
"The contextvar value set on the worker would not propagate back to the main"
|
|
685
|
+
" thread"
|
|
686
|
+
)
|
|
687
|
+
assert sniffio.current_async_library() == "trio"
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
async def test_trio_from_thread_run_sync() -> None:
|
|
691
|
+
# Test that to_thread_run_sync correctly "hands off" the trio token to
|
|
692
|
+
# trio.from_thread.run_sync()
|
|
693
|
+
def thread_fn_1() -> float:
|
|
694
|
+
trio_time = from_thread_run_sync(_core.current_time)
|
|
695
|
+
return trio_time
|
|
696
|
+
|
|
697
|
+
trio_time = await to_thread_run_sync(thread_fn_1)
|
|
698
|
+
assert isinstance(trio_time, float)
|
|
699
|
+
|
|
700
|
+
# Test correct error when passed async function
|
|
701
|
+
async def async_fn() -> None: # pragma: no cover
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
def thread_fn_2() -> None:
|
|
705
|
+
from_thread_run_sync(async_fn) # type: ignore[unused-coroutine]
|
|
706
|
+
|
|
707
|
+
with pytest.raises(TypeError, match="expected a synchronous function"):
|
|
708
|
+
await to_thread_run_sync(thread_fn_2)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
async def test_trio_from_thread_run() -> None:
|
|
712
|
+
# Test that to_thread_run_sync correctly "hands off" the trio token to
|
|
713
|
+
# trio.from_thread.run()
|
|
714
|
+
record = []
|
|
715
|
+
|
|
716
|
+
async def back_in_trio_fn() -> None:
|
|
717
|
+
_core.current_time() # implicitly checks that we're in trio
|
|
718
|
+
record.append("back in trio")
|
|
719
|
+
|
|
720
|
+
def thread_fn() -> None:
|
|
721
|
+
record.append("in thread")
|
|
722
|
+
from_thread_run(back_in_trio_fn)
|
|
723
|
+
|
|
724
|
+
await to_thread_run_sync(thread_fn)
|
|
725
|
+
assert record == ["in thread", "back in trio"]
|
|
726
|
+
|
|
727
|
+
# Test correct error when passed sync function
|
|
728
|
+
def sync_fn() -> None: # pragma: no cover
|
|
729
|
+
pass
|
|
730
|
+
|
|
731
|
+
with pytest.raises(TypeError, match="appears to be synchronous"):
|
|
732
|
+
await to_thread_run_sync(from_thread_run, sync_fn) # type: ignore[arg-type]
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
async def test_trio_from_thread_token() -> None:
|
|
736
|
+
# Test that to_thread_run_sync and spawned trio.from_thread.run_sync()
|
|
737
|
+
# share the same Trio token
|
|
738
|
+
def thread_fn() -> _core.TrioToken:
|
|
739
|
+
callee_token = from_thread_run_sync(_core.current_trio_token)
|
|
740
|
+
return callee_token
|
|
741
|
+
|
|
742
|
+
caller_token = _core.current_trio_token()
|
|
743
|
+
callee_token = await to_thread_run_sync(thread_fn)
|
|
744
|
+
assert callee_token == caller_token
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
async def test_trio_from_thread_token_kwarg() -> None:
|
|
748
|
+
# Test that to_thread_run_sync and spawned trio.from_thread.run_sync() can
|
|
749
|
+
# use an explicitly defined token
|
|
750
|
+
def thread_fn(token: _core.TrioToken) -> _core.TrioToken:
|
|
751
|
+
callee_token = from_thread_run_sync(_core.current_trio_token, trio_token=token)
|
|
752
|
+
return callee_token
|
|
753
|
+
|
|
754
|
+
caller_token = _core.current_trio_token()
|
|
755
|
+
callee_token = await to_thread_run_sync(thread_fn, caller_token)
|
|
756
|
+
assert callee_token == caller_token
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
async def test_from_thread_no_token() -> None:
|
|
760
|
+
# Test that a "raw call" to trio.from_thread.run() fails because no token
|
|
761
|
+
# has been provided
|
|
762
|
+
|
|
763
|
+
with pytest.raises(RuntimeError):
|
|
764
|
+
from_thread_run_sync(_core.current_time)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
async def test_trio_from_thread_run_sync_contextvars() -> None:
|
|
768
|
+
trio_test_contextvar.set("main")
|
|
769
|
+
|
|
770
|
+
def thread_fn() -> tuple[str, str, str, str, str]:
|
|
771
|
+
thread_parent_value = trio_test_contextvar.get()
|
|
772
|
+
trio_test_contextvar.set("worker")
|
|
773
|
+
thread_current_value = trio_test_contextvar.get()
|
|
774
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
775
|
+
sniffio.current_async_library()
|
|
776
|
+
|
|
777
|
+
def back_in_main() -> tuple[str, str]:
|
|
778
|
+
back_parent_value = trio_test_contextvar.get()
|
|
779
|
+
trio_test_contextvar.set("back_in_main")
|
|
780
|
+
back_current_value = trio_test_contextvar.get()
|
|
781
|
+
assert sniffio.current_async_library() == "trio"
|
|
782
|
+
return back_parent_value, back_current_value
|
|
783
|
+
|
|
784
|
+
back_parent_value, back_current_value = from_thread_run_sync(back_in_main)
|
|
785
|
+
thread_after_value = trio_test_contextvar.get()
|
|
786
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
787
|
+
sniffio.current_async_library()
|
|
788
|
+
return (
|
|
789
|
+
thread_parent_value,
|
|
790
|
+
thread_current_value,
|
|
791
|
+
thread_after_value,
|
|
792
|
+
back_parent_value,
|
|
793
|
+
back_current_value,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
(
|
|
797
|
+
thread_parent_value,
|
|
798
|
+
thread_current_value,
|
|
799
|
+
thread_after_value,
|
|
800
|
+
back_parent_value,
|
|
801
|
+
back_current_value,
|
|
802
|
+
) = await to_thread_run_sync(thread_fn)
|
|
803
|
+
current_value = trio_test_contextvar.get()
|
|
804
|
+
assert current_value == thread_parent_value == "main"
|
|
805
|
+
assert thread_current_value == back_parent_value == thread_after_value == "worker"
|
|
806
|
+
assert sniffio.current_async_library() == "trio"
|
|
807
|
+
assert back_current_value == "back_in_main"
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
async def test_trio_from_thread_run_contextvars() -> None:
|
|
811
|
+
trio_test_contextvar.set("main")
|
|
812
|
+
|
|
813
|
+
def thread_fn() -> tuple[str, str, str, str, str]:
|
|
814
|
+
thread_parent_value = trio_test_contextvar.get()
|
|
815
|
+
trio_test_contextvar.set("worker")
|
|
816
|
+
thread_current_value = trio_test_contextvar.get()
|
|
817
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
818
|
+
sniffio.current_async_library()
|
|
819
|
+
|
|
820
|
+
async def async_back_in_main() -> tuple[str, str]:
|
|
821
|
+
back_parent_value = trio_test_contextvar.get()
|
|
822
|
+
trio_test_contextvar.set("back_in_main")
|
|
823
|
+
back_current_value = trio_test_contextvar.get()
|
|
824
|
+
assert sniffio.current_async_library() == "trio"
|
|
825
|
+
return back_parent_value, back_current_value
|
|
826
|
+
|
|
827
|
+
back_parent_value, back_current_value = from_thread_run(async_back_in_main)
|
|
828
|
+
thread_after_value = trio_test_contextvar.get()
|
|
829
|
+
with pytest.raises(sniffio.AsyncLibraryNotFoundError):
|
|
830
|
+
sniffio.current_async_library()
|
|
831
|
+
return (
|
|
832
|
+
thread_parent_value,
|
|
833
|
+
thread_current_value,
|
|
834
|
+
thread_after_value,
|
|
835
|
+
back_parent_value,
|
|
836
|
+
back_current_value,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
(
|
|
840
|
+
thread_parent_value,
|
|
841
|
+
thread_current_value,
|
|
842
|
+
thread_after_value,
|
|
843
|
+
back_parent_value,
|
|
844
|
+
back_current_value,
|
|
845
|
+
) = await to_thread_run_sync(thread_fn)
|
|
846
|
+
current_value = trio_test_contextvar.get()
|
|
847
|
+
assert current_value == thread_parent_value == "main"
|
|
848
|
+
assert thread_current_value == back_parent_value == thread_after_value == "worker"
|
|
849
|
+
assert back_current_value == "back_in_main"
|
|
850
|
+
assert sniffio.current_async_library() == "trio"
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def test_run_fn_as_system_task_caught_badly_typed_token() -> None:
|
|
854
|
+
with pytest.raises(RuntimeError):
|
|
855
|
+
from_thread_run_sync(
|
|
856
|
+
_core.current_time,
|
|
857
|
+
trio_token="Not TrioTokentype", # type: ignore[arg-type]
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
async def test_from_thread_inside_trio_thread() -> None:
|
|
862
|
+
def not_called() -> None: # pragma: no cover
|
|
863
|
+
raise AssertionError()
|
|
864
|
+
|
|
865
|
+
trio_token = _core.current_trio_token()
|
|
866
|
+
with pytest.raises(RuntimeError):
|
|
867
|
+
from_thread_run_sync(not_called, trio_token=trio_token)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def test_from_thread_run_during_shutdown() -> None:
|
|
871
|
+
save = []
|
|
872
|
+
record = []
|
|
873
|
+
|
|
874
|
+
async def agen(token: _core.TrioToken | None) -> AsyncGenerator[None, None]:
|
|
875
|
+
try:
|
|
876
|
+
yield
|
|
877
|
+
finally:
|
|
878
|
+
with _core.CancelScope(shield=True):
|
|
879
|
+
try:
|
|
880
|
+
await to_thread_run_sync(
|
|
881
|
+
partial(from_thread_run, sleep, 0, trio_token=token),
|
|
882
|
+
)
|
|
883
|
+
except _core.RunFinishedError:
|
|
884
|
+
record.append("finished")
|
|
885
|
+
else:
|
|
886
|
+
record.append("clean")
|
|
887
|
+
|
|
888
|
+
async def main(use_system_task: bool) -> None:
|
|
889
|
+
save.append(agen(_core.current_trio_token() if use_system_task else None))
|
|
890
|
+
await save[-1].asend(None)
|
|
891
|
+
|
|
892
|
+
_core.run(main, True) # System nursery will be closed and raise RunFinishedError
|
|
893
|
+
_core.run(main, False) # host task will be rescheduled as normal
|
|
894
|
+
assert record == ["finished", "clean"]
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
async def test_trio_token_weak_referenceable() -> None:
|
|
898
|
+
token = _core.current_trio_token()
|
|
899
|
+
assert isinstance(token, _core.TrioToken)
|
|
900
|
+
weak_reference = weakref.ref(token)
|
|
901
|
+
assert token is weak_reference()
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
async def test_unsafe_abandon_on_cancel_kwarg() -> None:
|
|
905
|
+
# This is a stand in for a numpy ndarray or other objects
|
|
906
|
+
# that (maybe surprisingly) lack a notion of truthiness
|
|
907
|
+
class BadBool:
|
|
908
|
+
def __bool__(self) -> bool:
|
|
909
|
+
raise NotImplementedError
|
|
910
|
+
|
|
911
|
+
with pytest.raises(NotImplementedError):
|
|
912
|
+
await to_thread_run_sync(int, abandon_on_cancel=BadBool()) # type: ignore[arg-type]
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
async def test_from_thread_reuses_task() -> None:
|
|
916
|
+
task = _core.current_task()
|
|
917
|
+
|
|
918
|
+
async def async_current_task() -> _core.Task:
|
|
919
|
+
return _core.current_task()
|
|
920
|
+
|
|
921
|
+
assert task is await to_thread_run_sync(from_thread_run_sync, _core.current_task)
|
|
922
|
+
assert task is await to_thread_run_sync(from_thread_run, async_current_task)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
async def test_recursive_to_thread() -> None:
|
|
926
|
+
tid = None
|
|
927
|
+
|
|
928
|
+
def get_tid_then_reenter() -> int:
|
|
929
|
+
nonlocal tid
|
|
930
|
+
tid = threading.get_ident()
|
|
931
|
+
return from_thread_run(to_thread_run_sync, threading.get_ident)
|
|
932
|
+
|
|
933
|
+
assert tid != await to_thread_run_sync(get_tid_then_reenter)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
async def test_from_thread_host_cancelled() -> None:
|
|
937
|
+
queue: stdlib_queue.Queue[bool] = stdlib_queue.Queue()
|
|
938
|
+
|
|
939
|
+
def sync_check() -> None:
|
|
940
|
+
from_thread_run_sync(cancel_scope.cancel)
|
|
941
|
+
try:
|
|
942
|
+
from_thread_run_sync(bool)
|
|
943
|
+
except _core.Cancelled: # pragma: no cover
|
|
944
|
+
queue.put(True) # sync functions don't raise Cancelled
|
|
945
|
+
else:
|
|
946
|
+
queue.put(False)
|
|
947
|
+
|
|
948
|
+
with _core.CancelScope() as cancel_scope:
|
|
949
|
+
await to_thread_run_sync(sync_check)
|
|
950
|
+
|
|
951
|
+
assert not cancel_scope.cancelled_caught
|
|
952
|
+
assert not queue.get_nowait()
|
|
953
|
+
|
|
954
|
+
with _core.CancelScope() as cancel_scope:
|
|
955
|
+
await to_thread_run_sync(sync_check, abandon_on_cancel=True)
|
|
956
|
+
|
|
957
|
+
assert cancel_scope.cancelled_caught
|
|
958
|
+
assert not await to_thread_run_sync(partial(queue.get, timeout=1))
|
|
959
|
+
|
|
960
|
+
async def no_checkpoint() -> bool:
|
|
961
|
+
return True
|
|
962
|
+
|
|
963
|
+
def async_check() -> None:
|
|
964
|
+
from_thread_run_sync(cancel_scope.cancel)
|
|
965
|
+
try:
|
|
966
|
+
assert from_thread_run(no_checkpoint)
|
|
967
|
+
except _core.Cancelled: # pragma: no cover
|
|
968
|
+
queue.put(True) # async functions raise Cancelled at checkpoints
|
|
969
|
+
else:
|
|
970
|
+
queue.put(False)
|
|
971
|
+
|
|
972
|
+
with _core.CancelScope() as cancel_scope:
|
|
973
|
+
await to_thread_run_sync(async_check)
|
|
974
|
+
|
|
975
|
+
assert not cancel_scope.cancelled_caught
|
|
976
|
+
assert not queue.get_nowait()
|
|
977
|
+
|
|
978
|
+
with _core.CancelScope() as cancel_scope:
|
|
979
|
+
await to_thread_run_sync(async_check, abandon_on_cancel=True)
|
|
980
|
+
|
|
981
|
+
assert cancel_scope.cancelled_caught
|
|
982
|
+
assert not await to_thread_run_sync(partial(queue.get, timeout=1))
|
|
983
|
+
|
|
984
|
+
async def async_time_bomb() -> None:
|
|
985
|
+
cancel_scope.cancel()
|
|
986
|
+
with fail_after(10):
|
|
987
|
+
await sleep_forever()
|
|
988
|
+
|
|
989
|
+
with _core.CancelScope() as cancel_scope:
|
|
990
|
+
await to_thread_run_sync(from_thread_run, async_time_bomb)
|
|
991
|
+
|
|
992
|
+
assert cancel_scope.cancelled_caught
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
async def child(
|
|
996
|
+
abandon_on_cancel: bool,
|
|
997
|
+
scope: CancelScope,
|
|
998
|
+
record: list[str],
|
|
999
|
+
f: Callable[[], None],
|
|
1000
|
+
) -> None:
|
|
1001
|
+
with scope:
|
|
1002
|
+
record.append("start")
|
|
1003
|
+
try:
|
|
1004
|
+
return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel)
|
|
1005
|
+
except _core.Cancelled as e:
|
|
1006
|
+
record.append(str(e))
|
|
1007
|
+
raise
|
|
1008
|
+
finally:
|
|
1009
|
+
record.append("exit")
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@pytest.mark.parametrize("cancel_the_scope", [False, True])
|
|
1013
|
+
async def test_from_thread_check_cancelled_no_abandon(cancel_the_scope: bool) -> None:
|
|
1014
|
+
q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
|
|
1015
|
+
|
|
1016
|
+
def f() -> None:
|
|
1017
|
+
try:
|
|
1018
|
+
from_thread_check_cancelled()
|
|
1019
|
+
except _core.Cancelled as e: # pragma: no cover, test failure path
|
|
1020
|
+
q.put(str(e))
|
|
1021
|
+
else:
|
|
1022
|
+
q.put("Not Cancelled")
|
|
1023
|
+
ev.wait()
|
|
1024
|
+
return from_thread_check_cancelled()
|
|
1025
|
+
|
|
1026
|
+
record: list[str] = []
|
|
1027
|
+
ev = threading.Event()
|
|
1028
|
+
scope = _core.CancelScope() # Nursery cancel scope gives false positives
|
|
1029
|
+
|
|
1030
|
+
async with _core.open_nursery() as nursery:
|
|
1031
|
+
nursery.start_soon(child, False, scope, record, f)
|
|
1032
|
+
await wait_all_tasks_blocked()
|
|
1033
|
+
assert record[0] == "start"
|
|
1034
|
+
assert q.get(timeout=1) == "Not Cancelled"
|
|
1035
|
+
if cancel_the_scope:
|
|
1036
|
+
scope.cancel()
|
|
1037
|
+
ev.set()
|
|
1038
|
+
# Base case: nothing cancelled so we shouldn't see cancels anywhere
|
|
1039
|
+
if not cancel_the_scope:
|
|
1040
|
+
# implicit assertion, Cancelled not raised via nursery
|
|
1041
|
+
assert record[1] == "exit"
|
|
1042
|
+
else:
|
|
1043
|
+
# abandon_on_cancel=False case: a cancel will pop out but be handled by
|
|
1044
|
+
# the appropriate cancel scope
|
|
1045
|
+
|
|
1046
|
+
assert scope.cancelled_caught
|
|
1047
|
+
assert re.fullmatch(
|
|
1048
|
+
r"cancelled due to explicit from task "
|
|
1049
|
+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_no_abandon' at 0x\w*>",
|
|
1050
|
+
record[1],
|
|
1051
|
+
), record[1]
|
|
1052
|
+
assert record[2] == "exit"
|
|
1053
|
+
assert len(record) == 3
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
async def test_from_thread_check_cancelled_abandon_on_cancel() -> None:
|
|
1057
|
+
q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
|
|
1058
|
+
# abandon_on_cancel=True case: slightly different thread behavior needed
|
|
1059
|
+
# check thread is cancelled "soon" after abandonment
|
|
1060
|
+
|
|
1061
|
+
def f() -> None:
|
|
1062
|
+
ev.wait()
|
|
1063
|
+
try:
|
|
1064
|
+
from_thread_check_cancelled()
|
|
1065
|
+
except _core.Cancelled as e:
|
|
1066
|
+
q.put(str(e))
|
|
1067
|
+
except BaseException as e: # pragma: no cover, test failure path
|
|
1068
|
+
# abandon_on_cancel=True will eat exceptions, so we pass it
|
|
1069
|
+
# through the queue in order to be able to debug any exceptions
|
|
1070
|
+
q.put(e)
|
|
1071
|
+
else: # pragma: no cover, test failure path
|
|
1072
|
+
q.put("Not Cancelled")
|
|
1073
|
+
|
|
1074
|
+
record: list[str] = []
|
|
1075
|
+
ev = threading.Event()
|
|
1076
|
+
scope = _core.CancelScope()
|
|
1077
|
+
async with _core.open_nursery() as nursery:
|
|
1078
|
+
nursery.start_soon(child, True, scope, record, f)
|
|
1079
|
+
await wait_all_tasks_blocked()
|
|
1080
|
+
assert record[0] == "start"
|
|
1081
|
+
scope.cancel()
|
|
1082
|
+
# In the worst case the nursery fully exits before the threaded function
|
|
1083
|
+
# checks for cancellation.
|
|
1084
|
+
ev.set()
|
|
1085
|
+
|
|
1086
|
+
assert scope.cancelled_caught
|
|
1087
|
+
assert re.fullmatch(
|
|
1088
|
+
r"cancelled due to explicit from task "
|
|
1089
|
+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
|
|
1090
|
+
record[1],
|
|
1091
|
+
), record[1]
|
|
1092
|
+
assert record[-1] == "exit"
|
|
1093
|
+
res = q.get(timeout=1)
|
|
1094
|
+
if isinstance(res, BaseException): # pragma: no cover # for debugging
|
|
1095
|
+
raise res
|
|
1096
|
+
else:
|
|
1097
|
+
assert re.fullmatch(
|
|
1098
|
+
r"cancelled due to explicit from task "
|
|
1099
|
+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
|
|
1100
|
+
res,
|
|
1101
|
+
), res
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def test_from_thread_check_cancelled_raises_in_foreign_threads() -> None:
|
|
1105
|
+
with pytest.raises(RuntimeError):
|
|
1106
|
+
from_thread_check_cancelled()
|
|
1107
|
+
q: stdlib_queue.Queue[Outcome[object]] = stdlib_queue.Queue()
|
|
1108
|
+
_core.start_thread_soon(from_thread_check_cancelled, lambda _: q.put(_))
|
|
1109
|
+
with pytest.raises(RuntimeError):
|
|
1110
|
+
q.get(timeout=1).unwrap()
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
@slow
|
|
1114
|
+
async def test_reentry_doesnt_deadlock() -> None:
|
|
1115
|
+
# Regression test for issue noticed in GH-2827
|
|
1116
|
+
# The failure mode is to hang the whole test suite, unfortunately.
|
|
1117
|
+
# XXX consider running this in a subprocess with a timeout, if it comes up again!
|
|
1118
|
+
|
|
1119
|
+
async def child() -> None:
|
|
1120
|
+
while True:
|
|
1121
|
+
await to_thread_run_sync(from_thread_run, sleep, 0, abandon_on_cancel=False)
|
|
1122
|
+
|
|
1123
|
+
with move_on_after(2):
|
|
1124
|
+
async with _core.open_nursery() as nursery:
|
|
1125
|
+
for _ in range(4):
|
|
1126
|
+
nursery.start_soon(child)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
async def test_wait_all_threads_completed() -> None:
|
|
1130
|
+
no_threads_left = False
|
|
1131
|
+
e1 = Event()
|
|
1132
|
+
e2 = Event()
|
|
1133
|
+
|
|
1134
|
+
e1_exited = Event()
|
|
1135
|
+
e2_exited = Event()
|
|
1136
|
+
|
|
1137
|
+
async def wait_event(e: Event, e_exit: Event) -> None:
|
|
1138
|
+
def thread() -> None:
|
|
1139
|
+
from_thread_run(e.wait)
|
|
1140
|
+
|
|
1141
|
+
await to_thread_run_sync(thread)
|
|
1142
|
+
e_exit.set()
|
|
1143
|
+
|
|
1144
|
+
async def wait_no_threads_left() -> None:
|
|
1145
|
+
nonlocal no_threads_left
|
|
1146
|
+
await wait_all_threads_completed()
|
|
1147
|
+
no_threads_left = True
|
|
1148
|
+
|
|
1149
|
+
async with _core.open_nursery() as nursery:
|
|
1150
|
+
nursery.start_soon(wait_event, e1, e1_exited)
|
|
1151
|
+
nursery.start_soon(wait_event, e2, e2_exited)
|
|
1152
|
+
await wait_all_tasks_blocked()
|
|
1153
|
+
nursery.start_soon(wait_no_threads_left)
|
|
1154
|
+
await wait_all_tasks_blocked()
|
|
1155
|
+
assert not no_threads_left
|
|
1156
|
+
assert active_thread_count() == 2
|
|
1157
|
+
|
|
1158
|
+
e1.set()
|
|
1159
|
+
await e1_exited.wait()
|
|
1160
|
+
await wait_all_tasks_blocked()
|
|
1161
|
+
assert not no_threads_left
|
|
1162
|
+
assert active_thread_count() == 1
|
|
1163
|
+
|
|
1164
|
+
e2.set()
|
|
1165
|
+
await e2_exited.wait()
|
|
1166
|
+
await wait_all_tasks_blocked()
|
|
1167
|
+
assert no_threads_left
|
|
1168
|
+
assert active_thread_count() == 0
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
async def test_wait_all_threads_completed_no_threads() -> None:
|
|
1172
|
+
await wait_all_threads_completed()
|
|
1173
|
+
assert active_thread_count() == 0
|