Skip to content

Commit ad5d7cd

Browse files
committed
added more tests
1 parent f80e6e7 commit ad5d7cd

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

custom_components/pyscript/trigger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ async def wait_until(
145145
event_trig_expr.parse(event_trigger[1])
146146
exc = event_trig_expr.get_exception_obj()
147147
if exc is not None:
148-
if len(state_trig_ident) > 0:
148+
if state_trig_ident:
149149
State.notify_del(state_trig_ident, notify_q)
150150
raise exc
151151
Event.notify_add(event_trigger[0], notify_q)

tests/test_decorator_errors.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Test pyscript decorator syntax error and eval-time exception reporting."""
2+
from ast import literal_eval
3+
import asyncio
4+
from datetime import datetime as dt
5+
import pathlib
6+
7+
from custom_components.pyscript.const import DOMAIN
8+
import custom_components.pyscript.trigger as trigger
9+
from pytest_homeassistant.async_mock import mock_open, patch
10+
11+
from homeassistant import loader
12+
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED
13+
from homeassistant.setup import async_setup_component
14+
15+
16+
async def setup_script(hass, notify_q, now, source):
17+
"""Initialize and load the given pyscript."""
18+
scripts = [
19+
"/some/config/dir/pyscripts/hello.py",
20+
]
21+
integration = loader.Integration(
22+
hass,
23+
"custom_components.pyscript",
24+
pathlib.Path("custom_components/pyscript"),
25+
{"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"},
26+
)
27+
28+
with patch("homeassistant.loader.async_get_integration", return_value=integration), patch(
29+
"custom_components.pyscript.os.path.isdir", return_value=True
30+
), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch(
31+
"custom_components.pyscript.open", mock_open(read_data=source), create=True,
32+
), patch(
33+
"custom_components.pyscript.trigger.dt_now", return_value=now
34+
):
35+
assert await async_setup_component(hass, "pyscript", {DOMAIN: {}})
36+
37+
#
38+
# I'm not sure how to run the mock all the time, so just force the dt_now()
39+
# trigger function to return the given list of times in now.
40+
#
41+
def return_next_time():
42+
nonlocal now
43+
if isinstance(now, list):
44+
if len(now) > 1:
45+
return now.pop(0)
46+
return now[0]
47+
return now
48+
49+
trigger.__dict__["dt_now"] = return_next_time
50+
51+
if notify_q:
52+
53+
async def state_changed(event):
54+
var_name = event.data["entity_id"]
55+
if var_name != "pyscript.done":
56+
return
57+
value = event.data["new_state"].state
58+
await notify_q.put(value)
59+
60+
hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)
61+
62+
63+
async def wait_until_done(notify_q):
64+
"""Wait for the done handshake."""
65+
return await asyncio.wait_for(notify_q.get(), timeout=4)
66+
67+
68+
async def test_decorator_errors(hass, caplog):
69+
"""Test decorator syntax and run-time errors."""
70+
notify_q = asyncio.Queue(0)
71+
await setup_script(
72+
hass,
73+
notify_q,
74+
[dt(2020, 7, 1, 10, 59, 59, 999999), dt(2020, 7, 1, 11, 59, 59, 999999)],
75+
"""
76+
seq_num = 0
77+
78+
@time_trigger("startup")
79+
def func_startup_sync(trigger_type=None, trigger_time=None):
80+
global seq_num
81+
82+
seq_num += 1
83+
log.info(f"func_startup_sync setting pyscript.done = {seq_num}, trigger_type = {trigger_type}, trigger_time = {trigger_time}")
84+
pyscript.done = seq_num
85+
86+
@state_trigger("z + ")
87+
def func1():
88+
pass
89+
90+
@event_trigger("some_event", "func(")
91+
def func2():
92+
pass
93+
94+
@state_trigger("True")
95+
@state_active("z + ")
96+
def func3():
97+
pass
98+
99+
@state_active("z + ")
100+
def func4():
101+
pass
102+
103+
@state_trigger("1 / int(pyscript.var1)")
104+
def func5():
105+
pass
106+
107+
@state_trigger("True or pyscript.var1")
108+
@state_active("1 / pyscript.var1")
109+
def func6():
110+
pass
111+
112+
@state_trigger("pyscript.var7")
113+
def func7():
114+
global seq_num
115+
116+
try:
117+
task.wait_until(state_trigger="z +")
118+
except SyntaxError as exc:
119+
log.error(exc)
120+
121+
try:
122+
task.wait_until(event_trigger=["event", "z+"])
123+
except SyntaxError as exc:
124+
log.error(exc)
125+
126+
try:
127+
task.wait_until(state_trigger="pyscript.var1 + 1")
128+
except TypeError as exc:
129+
log.error(exc)
130+
131+
seq_num += 1
132+
pyscript.done = seq_num
133+
134+
@state_trigger("pyscript.var_done")
135+
def func_wrapup():
136+
global seq_num
137+
138+
seq_num += 1
139+
pyscript.done = seq_num
140+
141+
""",
142+
)
143+
seq_num = 0
144+
145+
seq_num += 1
146+
# fire event to start triggers, and handshake when they are running
147+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
148+
assert literal_eval(await wait_until_done(notify_q)) == seq_num
149+
150+
hass.states.async_set("pyscript.var1", 1)
151+
hass.states.async_set("pyscript.var1", 0)
152+
153+
seq_num += 1
154+
hass.states.async_set("pyscript.var7", 1)
155+
assert literal_eval(await wait_until_done(notify_q)) == seq_num
156+
157+
seq_num += 1
158+
hass.states.async_set("pyscript.var_done", 1)
159+
assert literal_eval(await wait_until_done(notify_q)) == seq_num
160+
161+
assert "SyntaxError: invalid syntax (file.hello.func1 @state_trigger(), line 1)" in caplog.text
162+
assert (
163+
"SyntaxError: unexpected EOF while parsing (file.hello.func2 @event_trigger(), line 1)"
164+
in caplog.text
165+
)
166+
assert "SyntaxError: invalid syntax (file.hello.func3 @state_active(), line 1)" in caplog.text
167+
assert (
168+
"func4 defined in file.hello: needs at least one trigger decorator (ie: event_trigger, state_trigger, time_trigger)"
169+
in caplog.text
170+
)
171+
assert (
172+
"""Exception in <file.hello.func5 @state_trigger()> line 1:
173+
1 / int(pyscript.var1)
174+
^
175+
ZeroDivisionError: division by zero"""
176+
in caplog.text
177+
)
178+
179+
assert (
180+
"""Exception in <file.hello.func6 @state_active()> line 1:
181+
1 / pyscript.var1
182+
^
183+
TypeError: unsupported operand type(s) for /: 'int' and 'str'"""
184+
in caplog.text
185+
)
186+
187+
assert (
188+
"""Exception in <file.hello.func6 @state_active()> line 1:
189+
1 / pyscript.var1
190+
^
191+
TypeError: unsupported operand type(s) for /: 'int' and 'str'"""
192+
in caplog.text
193+
)
194+
195+
assert "invalid syntax (file.hello.func7 state_trigger, line 1)" in caplog.text
196+
assert "invalid syntax (file.hello.func7 event_trigger, line 1)" in caplog.text
197+
assert 'can only concatenate str (not "int") to str' in caplog.text

0 commit comments

Comments
 (0)