-
Notifications
You must be signed in to change notification settings - Fork 60
/
test_poll.py
262 lines (199 loc) · 7.98 KB
/
test_poll.py
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""Tests for polling-related functionality."""
import os
import tempfile
from enum import Enum
from random import random
from types import TracebackType
from typing import Any, Dict, Callable, Optional, Type, cast
import pytest
from shiny import *
from shiny import _utils
from shiny.reactive import *
from shiny._namespaces import Root
from .mocktime import MockTime
class OnEndedSessionCallbacks:
"""
A far-too-minimal mock of Session that implements nothing but on_ended. This is
used so that invalidate_later calls can be cleaned up, otherwise you get warnings
about pending tasks when pytest completes.
Eventually we should have a proper mock of Session, then we can retire this.
"""
ns = Root
def __init__(self):
self._on_ended_callbacks = _utils.Callbacks()
# Unfortunately we have to lie here and say we're a session. Obvously, any
# attempt to call anything but session.on_ended() will fail.
self._session_context = session.session_context(cast(Session, self))
def on_ended(self, fn: Callable[[], None]) -> Callable[[], None]:
return self._on_ended_callbacks.register(fn)
def _send_message_sync(self, message: Dict[str, object]) -> None:
pass
async def __aenter__(self):
self._session_context.__enter__()
async def __aexit__(
self,
exctype: Optional[Type[BaseException]],
excinst: Optional[BaseException],
exctb: Optional[TracebackType],
):
self._session_context.__exit__(exctype, excinst, exctb)
self._on_ended_callbacks.invoke()
@pytest.mark.asyncio
async def test_poll():
async with OnEndedSessionCallbacks():
poll_invocations = 0
poll_return1 = 0 # A non-reactive component of the return value
poll_return2 = Value(0) # A reactive component of the return value
value_invocations = 0
value_dep = Value(0)
def poll_func():
nonlocal poll_invocations
poll_invocations += 1
return poll_return1 + poll_return2()
@poll(poll_func)
def value_func():
value_dep() # Take a reactive dependency on value_dep
nonlocal value_invocations
value_invocations += 1
mock_time = MockTime()
with mock_time():
# Poll func is invoked once during @poll(), to seed the underlying reactive val
assert (poll_invocations, value_invocations) == (1, 0)
await flush()
# The observer that updates poll has executed once.
# @poll returns a lazy Calc, so value hasn't been invoked yet.
assert (poll_invocations, value_invocations) == (2, 0)
@Effect()
def _():
value_func()
await flush()
assert (poll_invocations, value_invocations) == (2, 1)
await flush()
assert (poll_invocations, value_invocations) == (2, 1)
await mock_time.advance_time(1.01)
# poll_invocations advances without a flush() because invalidate_later itself
# invokes flush()
assert (poll_invocations, value_invocations) == (3, 1)
await flush()
assert (poll_invocations, value_invocations) == (3, 1)
# Now change value
poll_return1 += 1
await flush()
# Nothing changes because no time has passed, poll() has not tried again
assert (poll_invocations, value_invocations) == (3, 1)
await mock_time.advance_time(1.01)
assert (poll_invocations, value_invocations) == (4, 2)
# When a reactive dependency of poll_func invalidates, there's no need to wait
# until the next poll
with isolate():
poll_return2.set(poll_return2() + 1)
await flush()
assert (poll_invocations, value_invocations) == (5, 3)
# When a reactive dependency of value_func invalidates, there's no need to wait
# until the next poll
with isolate():
value_dep.set(value_dep() + 1)
await flush()
assert (poll_invocations, value_invocations) == (5, 4)
await mock_time.advance_time(1.01)
assert (poll_invocations, value_invocations) == (6, 4)
@pytest.mark.asyncio
async def test_poll_errors():
async with OnEndedSessionCallbacks():
class Mode(Enum):
NORMAL = 0
RAISE = 1
INCOMPARABLE = 2
class Incomparable:
def __eq__(self, other: Any):
raise ValueError("I refused to be compared to anyone!")
mock_time = MockTime()
with mock_time():
mode = Mode.NORMAL
def poll_func() -> Any:
if mode is Mode.NORMAL:
return random()
if mode is Mode.RAISE:
raise ValueError("boom")
if mode is Mode.INCOMPARABLE:
return Incomparable()
@poll(poll_func)
def bad_poll() -> Any:
return random()
invocations = 0
@Effect()
def _():
nonlocal invocations
invocations += 1
if mode is Mode.NORMAL:
bad_poll()
if mode is Mode.RAISE:
with pytest.raises(ValueError):
bad_poll()
if mode is Mode.INCOMPARABLE:
with pytest.raises(TypeError):
bad_poll()
with isolate():
await flush()
bad_poll()
mode = Mode.RAISE
# Doesn't raise because polling hasn't happened yet
bad_poll()
await mock_time.advance_time(1.01)
await flush()
with pytest.raises(ValueError):
bad_poll()
mode = Mode.INCOMPARABLE
await mock_time.advance_time(1.01)
await flush()
with pytest.raises(TypeError):
bad_poll()
assert invocations == 3
tmpfile = tempfile.NamedTemporaryFile()
@pytest.mark.asyncio
async def test_file_reader():
async with OnEndedSessionCallbacks():
invocations = 0
tmpfile.write("hello\n".encode())
tmpfile.flush()
mock_time = MockTime()
with mock_time():
@file_reader(tmpfile.name)
def read_file():
with open(tmpfile.name, "r") as f:
nonlocal invocations
invocations += 1
return f.read()
with isolate():
assert invocations == 0
await flush()
assert read_file() == "hello\n"
assert invocations == 1
# Advancing time without a write does nothing
await mock_time.advance_time(1.01)
assert read_file() == "hello\n"
assert invocations == 1
tmpfile.write("goodbye\n".encode())
tmpfile.flush()
# The file's been updated, but we haven't looked yet
assert read_file() == "hello\n"
await mock_time.advance_time(1.01)
assert read_file() == "hello\ngoodbye\n"
assert invocations == 2
@pytest.mark.asyncio
async def test_file_reader_error():
async with OnEndedSessionCallbacks():
tmpfile1 = tempfile.NamedTemporaryFile(delete=False)
mock_time = MockTime()
with mock_time():
@file_reader(tmpfile1.name)
def read_file():
return True
with isolate():
await flush()
assert read_file() is True
os.unlink(tmpfile1.name)
await mock_time.advance_time(1.01)
await flush()
with pytest.raises(FileNotFoundError):
read_file()