Skip to content

Commit f7331d3

Browse files
committed
adding test for custom dash server
1 parent 77e22a3 commit f7331d3

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import pytest
2+
from dash import Dash, Input, Output, html, dcc
3+
from fastapi import FastAPI
4+
import traceback
5+
import re
6+
from dash.backends._fastapi import FastAPIDashServer
7+
8+
9+
class CustomDashServer(FastAPIDashServer):
10+
def _get_traceback(self, _secret, error: Exception):
11+
tb = error.__traceback__
12+
errors = traceback.format_exception(type(error), error, tb)
13+
pass_errs = []
14+
callback_handled = False
15+
for err in errors:
16+
if self.error_handling_mode == "prune":
17+
if not callback_handled:
18+
if "callback invoked" in str(err) and "_callback.py" in str(err):
19+
callback_handled = True
20+
continue
21+
pass_errs.append(err)
22+
formatted_tb = "".join(pass_errs)
23+
error_type = type(error).__name__
24+
error_msg = str(error)
25+
# Parse traceback lines to group by file
26+
file_cards = []
27+
pattern = re.compile(r' File "(.+)", line (\d+), in (\w+)')
28+
lines = formatted_tb.split("\n")
29+
current_file = None
30+
card_lines = []
31+
for line in lines[:-1]: # Skip the last line (error message)
32+
match = pattern.match(line)
33+
if match:
34+
if current_file and card_lines:
35+
file_cards.append((current_file, card_lines))
36+
current_file = (
37+
f"{match.group(1)} (line {match.group(2)}, in {match.group(3)})"
38+
)
39+
card_lines = [line]
40+
elif current_file:
41+
card_lines.append(line)
42+
if current_file and card_lines:
43+
file_cards.append((current_file, card_lines))
44+
cards_html = ""
45+
for filename, card in file_cards:
46+
cards_html += (
47+
f"""
48+
<div class=\"error-card\">
49+
<div class=\"error-card-header\">{filename}</div>
50+
<pre class=\"error-card-traceback\">"""
51+
+ "\n".join(card)
52+
+ """</pre>
53+
</div>
54+
"""
55+
)
56+
html = f"""
57+
<!doctype html>
58+
<html lang=\"en\">
59+
<head>
60+
<title>{error_type}: {error_msg} // Custom Debugger</title>
61+
<style>
62+
body {{ font-family: monospace; background: #fff; color: #333; }}
63+
.debugger {{ margin: 2em; max-width: 700px; }}
64+
</style>
65+
</head>
66+
<body>
67+
<div class=\"debugger\">
68+
<h1>{error_type}: {error_msg}</h1>
69+
{cards_html}
70+
</div>
71+
</body>
72+
</html>
73+
"""
74+
return html
75+
76+
77+
@pytest.mark.parametrize(
78+
"fixture,input_value",
79+
[
80+
("dash_duo", "Hello CustomBackend!"),
81+
],
82+
)
83+
def test_custom_backend_basic_callback(request, fixture, input_value):
84+
dash_duo = request.getfixturevalue(fixture)
85+
app = Dash(__name__, backend=CustomDashServer)
86+
app.layout = html.Div(
87+
[dcc.Input(id="input", value=input_value, type="text"), html.Div(id="output")]
88+
)
89+
90+
@app.callback(Output("output", "children"), Input("input", "value"))
91+
def update_output(value):
92+
return f"You typed: {value}"
93+
94+
dash_duo.start_server(app)
95+
dash_duo.wait_for_text_to_equal("#output", f"You typed: {input_value}")
96+
dash_duo.clear_input(dash_duo.find_element("#input"))
97+
dash_duo.find_element("#input").send_keys("CustomBackend Test")
98+
dash_duo.wait_for_text_to_equal("#output", "You typed: CustomBackend Test")
99+
assert dash_duo.get_logs() == []
100+
101+
102+
@pytest.mark.parametrize(
103+
"fixture,start_server_kwargs",
104+
[
105+
("dash_duo", {"debug": True, "reload": False, "dev_tools_ui": True}),
106+
],
107+
)
108+
def test_custom_backend_error_handling(request, fixture, start_server_kwargs):
109+
dash_duo = request.getfixturevalue(fixture)
110+
app = Dash(__name__, backend=CustomDashServer)
111+
app.layout = html.Div(
112+
[html.Button(id="btn", children="Error", n_clicks=0), html.Div(id="output")]
113+
)
114+
115+
@app.callback(Output("output", "children"), Input("btn", "n_clicks"))
116+
def error_callback(n):
117+
if n and n > 0:
118+
return 1 / 0 # Intentional error
119+
return "No error"
120+
121+
dash_duo.start_server(app, **start_server_kwargs)
122+
dash_duo.wait_for_text_to_equal("#output", "No error")
123+
dash_duo.find_element("#btn").click()
124+
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1")
125+
126+
127+
def get_error_html(dash_duo, index):
128+
# error is in an iframe so is annoying to read out - get it from the store
129+
return dash_duo.driver.execute_script(
130+
"return store.getState().error.backEnd[{}].error.html;".format(index)
131+
)
132+
133+
134+
@pytest.mark.parametrize(
135+
"fixture,start_server_kwargs",
136+
[
137+
(
138+
"dash_duo",
139+
{
140+
"debug": True,
141+
"dev_tools_ui": True,
142+
"dev_tools_prune_errors": False,
143+
"reload": False,
144+
},
145+
),
146+
],
147+
)
148+
def test_custom_backend_error_handling_no_prune(request, fixture, start_server_kwargs):
149+
dash_duo = request.getfixturevalue(fixture)
150+
app = Dash(__name__, backend=CustomDashServer)
151+
app.layout = html.Div(
152+
[html.Button(id="btn", children="Error", n_clicks=0), html.Div(id="output")]
153+
)
154+
155+
@app.callback(Output("output", "children"), Input("btn", "n_clicks"))
156+
def error_callback(n):
157+
if n and n > 0:
158+
return 1 / 0 # Intentional error
159+
return "No error"
160+
161+
dash_duo.start_server(app, **start_server_kwargs)
162+
dash_duo.wait_for_text_to_equal("#output", "No error")
163+
dash_duo.find_element("#btn").click()
164+
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1")
165+
166+
error0 = get_error_html(dash_duo, 0)
167+
assert "Custom Debugger" in error0
168+
assert "in error_callback" in error0
169+
assert "ZeroDivisionError" in error0
170+
assert "_callback.py" in error0
171+
172+
173+
@pytest.mark.parametrize(
174+
"fixture,start_server_kwargs, error_msg",
175+
[
176+
("dash_duo", {"debug": True, "reload": False}, "custombackend.py"),
177+
],
178+
)
179+
def test_custom_backend_error_handling_prune(
180+
request, fixture, start_server_kwargs, error_msg
181+
):
182+
dash_duo = request.getfixturevalue(fixture)
183+
app = Dash(__name__, backend=CustomDashServer)
184+
app.layout = html.Div(
185+
[html.Button(id="btn", children="Error", n_clicks=0), html.Div(id="output")]
186+
)
187+
188+
@app.callback(Output("output", "children"), Input("btn", "n_clicks"))
189+
def error_callback(n):
190+
if n and n > 0:
191+
return 1 / 0 # Intentional error
192+
return "No error"
193+
194+
dash_duo.start_server(app, **start_server_kwargs)
195+
dash_duo.wait_for_text_to_equal("#output", "No error")
196+
dash_duo.find_element("#btn").click()
197+
dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1")
198+
199+
error0 = get_error_html(dash_duo, 0)
200+
assert "Custom Debugger" in error0
201+
assert "in error_callback" in error0
202+
assert "ZeroDivisionError" in error0
203+
assert "_callback.py" not in error0
204+
205+
206+
@pytest.mark.parametrize(
207+
"fixture,input_value",
208+
[
209+
("dash_duo", "Background CustomBackend!"),
210+
],
211+
)
212+
def test_custom_backend_background_callback(request, fixture, input_value):
213+
dash_duo = request.getfixturevalue(fixture)
214+
import diskcache
215+
216+
cache = diskcache.Cache("./cache")
217+
from dash.background_callback import DiskcacheManager
218+
219+
background_callback_manager = DiskcacheManager(cache)
220+
221+
app = Dash(
222+
__name__,
223+
backend=CustomDashServer,
224+
background_callback_manager=background_callback_manager,
225+
)
226+
app.layout = html.Div(
227+
[dcc.Input(id="input", value=input_value, type="text"), html.Div(id="output")]
228+
)
229+
230+
@app.callback(
231+
Output("output", "children"), Input("input", "value"), background=True
232+
)
233+
def update_output_bg(value):
234+
return f"Background typed: {value}"
235+
236+
dash_duo.start_server(app)
237+
dash_duo.wait_for_text_to_equal("#output", f"Background typed: {input_value}")
238+
dash_duo.clear_input(dash_duo.find_element("#input"))
239+
dash_duo.find_element("#input").send_keys("CustomBackend BG Test")
240+
dash_duo.wait_for_text_to_equal(
241+
"#output", "Background typed: CustomBackend BG Test"
242+
)
243+
assert dash_duo.get_logs() == []

0 commit comments

Comments
 (0)