Skip to content

Commit 1f77650

Browse files
Truncate the requirements.txt file before deploys (#998)
Co-authored-by: Barret Schloerke <[email protected]>
1 parent 62754c7 commit 1f77650

File tree

15 files changed

+341
-286
lines changed

15 files changed

+341
-286
lines changed

tests/playwright/deploys/apps/plotly_app/app_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pandas
22
plotly
33
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
44
git+https://github.com/posit-dev/py-shinywidgets.git#egg=shinywidgets
5+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
2+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
22
pandas
3+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
22
folium
3+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
2+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
2+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
2+
fastapi==0.108.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
2+
fastapi==0.108.0
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import os
2+
import sys
3+
import typing
4+
from pathlib import PurePath
5+
from typing import Literal
6+
7+
from conftest import run_shiny_app
8+
from playwright.sync_api import ConsoleMessage, Page
9+
10+
here_tests_e2e_examples = PurePath(__file__).parent
11+
pyshiny_root = here_tests_e2e_examples.parent.parent.parent
12+
13+
is_interactive = hasattr(sys, "ps1")
14+
reruns = 1 if is_interactive else 3
15+
16+
17+
def get_apps(path: str) -> typing.List[str]:
18+
full_path = pyshiny_root / path
19+
app_paths: typing.List[str] = []
20+
for folder in os.listdir(full_path):
21+
folder_path = os.path.join(full_path, folder)
22+
if os.path.isdir(folder_path):
23+
app_path = os.path.join(folder_path, "app.py")
24+
if os.path.isfile(app_path):
25+
# Return relative app path
26+
app_paths.append(os.path.join(path, folder, "app.py"))
27+
return app_paths
28+
29+
30+
example_apps: typing.List[str] = [
31+
*get_apps("examples"),
32+
*get_apps("shiny/api-examples"),
33+
*get_apps("shiny/templates/app-templates"),
34+
*get_apps("tests/playwright/deploys"),
35+
]
36+
37+
app_idle_wait = {"duration": 300, "timeout": 5 * 1000}
38+
app_hard_wait: typing.Dict[str, int] = {
39+
"brownian": 250,
40+
"ui-func": 250,
41+
}
42+
output_transformer_errors = [
43+
"ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`",
44+
" return OutputRenderer",
45+
# brownian example app
46+
"ShinyDeprecationWarning:",
47+
"shiny.render.transformer.output_transformer()",
48+
]
49+
express_warnings = ["Detected Shiny Express app. "]
50+
app_allow_shiny_errors: typing.Dict[
51+
str, typing.Union[Literal[True], typing.List[str]]
52+
] = {
53+
"SafeException": True,
54+
"global_pyplot": True,
55+
"static_plots": [
56+
# acceptable warning
57+
"PlotnineWarning: Smoothing requires 2 or more points",
58+
"RuntimeWarning: divide by zero encountered",
59+
"UserWarning: This figure includes Axes that are not compatible with tight_layout",
60+
],
61+
# Remove after shinywidgets accepts `Renderer` PR
62+
"airmass": [*output_transformer_errors],
63+
"brownian": [*output_transformer_errors],
64+
"multi-page": [*output_transformer_errors],
65+
"model-score": [*output_transformer_errors],
66+
"data_frame": [*output_transformer_errors],
67+
"output_transformer": [*output_transformer_errors],
68+
"render_display": [*express_warnings],
69+
}
70+
app_allow_external_errors: typing.List[str] = [
71+
# if shiny express app detected
72+
"Detected Shiny Express app",
73+
# plotnine: https://github.com/has2k1/plotnine/issues/713
74+
# mizani: https://github.com/has2k1/mizani/issues/34
75+
# seaborn: https://github.com/mwaskom/seaborn/issues/3457
76+
"FutureWarning: is_categorical_dtype is deprecated",
77+
"if pd.api.types.is_categorical_dtype(vector", # continutation of line above
78+
# plotnine: https://github.com/has2k1/plotnine/issues/713#issuecomment-1701363058
79+
"FutureWarning: The default of observed=False is deprecated",
80+
# seaborn: https://github.com/mwaskom/seaborn/pull/3355
81+
"FutureWarning: use_inf_as_na option is deprecated",
82+
"pd.option_context('mode.use_inf_as_na", # continutation of line above
83+
]
84+
app_allow_js_errors: typing.Dict[str, typing.List[str]] = {
85+
"brownian": ["Failed to acquire camera feed:"],
86+
}
87+
88+
89+
# Altered from `shinytest2:::app_wait_for_idle()`
90+
# https://github.com/rstudio/shinytest2/blob/b8fdce681597e9610fc078aa6e376134c404f3bd/R/app-driver-wait.R
91+
def wait_for_idle_js(duration: int = 500, timeout: int = 30 * 1000) -> str:
92+
return """
93+
let duration = %s; // time needed to be idle
94+
let timeout = %s; // max total time
95+
console.log("Making promise to run");
96+
new Promise((resolve, reject) => {
97+
console.log('Waiting for Shiny to be stable');
98+
const cleanup = () => {
99+
$(document).off('shiny:busy', busyFn);
100+
$(document).off('shiny:idle', idleFn);
101+
clearTimeout(timeoutId);
102+
clearTimeout(idleId);
103+
}
104+
let timeoutId = setTimeout(() => {
105+
cleanup();
106+
reject('Shiny did not become stable within ' + timeout + 'ms');
107+
}, +timeout); // make sure timeout is number
108+
let idleId = null;
109+
const busyFn = () => {
110+
// clear timeout. Calling with `null` is ok.
111+
clearTimeout(idleId);
112+
};
113+
const idleFn = () => {
114+
const fn = () => {
115+
// Made it through the required duration
116+
// Remove event listeners
117+
cleanup();
118+
console.log('Shiny has been idle for ' + duration + 'ms');
119+
// Resolve the promise
120+
resolve();
121+
};
122+
// delay the callback wrapper function
123+
idleId = setTimeout(fn, +duration);
124+
};
125+
// set up individual listeners for this function.
126+
$(document).on('shiny:busy', busyFn);
127+
$(document).on('shiny:idle', idleFn);
128+
// if already idle, call `idleFn` to kick things off.
129+
if (! $("html").hasClass("shiny-busy")) {
130+
idleFn();
131+
}
132+
})
133+
""" % (
134+
duration,
135+
timeout,
136+
)
137+
138+
139+
def wait_for_idle_app(
140+
page: Page, duration: int = 500, timeout: int = 30 * 1000
141+
) -> None:
142+
page.evaluate(
143+
wait_for_idle_js(duration, timeout),
144+
)
145+
146+
147+
def validate_example(page: Page, ex_app_path: str) -> None:
148+
app = run_shiny_app(pyshiny_root / ex_app_path, wait_for_start=True)
149+
150+
console_errors: typing.List[str] = []
151+
152+
def on_console_msg(msg: ConsoleMessage) -> None:
153+
if msg.type == "error":
154+
console_errors.append(msg.text)
155+
156+
page.on("console", on_console_msg)
157+
158+
# Makes sure the app is closed when exiting the code block
159+
with app:
160+
page.goto(app.url)
161+
162+
app_name = os.path.basename(os.path.dirname(ex_app_path))
163+
164+
if app_name in app_hard_wait.keys():
165+
# Apps are constantly invalidating and will not stabilize
166+
# Instead, wait for specific amount of time
167+
page.wait_for_timeout(app_hard_wait[app_name])
168+
else:
169+
# Wait for app to stop updating
170+
wait_for_idle_app(
171+
page,
172+
duration=app_idle_wait["duration"],
173+
timeout=app_idle_wait["timeout"],
174+
)
175+
176+
# Check for py-shiny errors
177+
error_lines = str(app.stderr).splitlines()
178+
179+
# Remove any errors that are allowed
180+
error_lines = [
181+
line
182+
for line in error_lines
183+
if not any([error_txt in line for error_txt in app_allow_external_errors])
184+
]
185+
186+
# Remove any app specific errors that are allowed
187+
if app_name in app_allow_shiny_errors:
188+
app_allowable_errors = app_allow_shiny_errors[app_name]
189+
else:
190+
app_allowable_errors = []
191+
192+
# If all errors are not allowed, check for unexpected errors
193+
if app_allowable_errors is not True:
194+
if isinstance(app_allowable_errors, str):
195+
app_allowable_errors = [app_allowable_errors]
196+
app_allowable_errors = (
197+
# Remove ^INFO lines
198+
["INFO:"]
199+
# Remove any known errors caused by external packages
200+
+ app_allow_external_errors
201+
# Remove any known errors allowed by the app
202+
+ app_allowable_errors
203+
)
204+
205+
# If there is an array of allowable errors, remove them from errors. Ex: `PlotnineWarning`
206+
error_lines = [
207+
line
208+
for line in error_lines
209+
if len(line.strip()) > 0
210+
and not any([error_txt in line for error_txt in app_allowable_errors])
211+
]
212+
if len(error_lines) > 0:
213+
print("\napp_allowable_errors :")
214+
print("\n".join(app_allowable_errors))
215+
print("\nError lines remaining:")
216+
print("\n".join(error_lines))
217+
assert len(error_lines) == 0
218+
219+
# Check for JavaScript errors
220+
if app_name in app_allow_js_errors:
221+
# Remove any errors that are allowed
222+
console_errors = [
223+
line
224+
for line in console_errors
225+
if not any(
226+
[error_txt in line for error_txt in app_allow_js_errors[app_name]]
227+
)
228+
]
229+
assert len(console_errors) == 0, (
230+
"In app "
231+
+ ex_app_path
232+
+ " had JavaScript console errors!\n"
233+
+ "* ".join(console_errors)
234+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import pytest
2+
from example_apps import get_apps, reruns, validate_example
3+
from playwright.sync_api import Page
4+
5+
6+
@pytest.mark.examples
7+
@pytest.mark.flaky(reruns=reruns, reruns_delay=1)
8+
@pytest.mark.parametrize("ex_app_path", get_apps("shiny/api-examples"))
9+
def test_api_examples(page: Page, ex_app_path: str) -> None:
10+
validate_example(page, ex_app_path)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import pytest
2+
from example_apps import get_apps, reruns, validate_example
3+
from playwright.sync_api import Page
4+
5+
6+
@pytest.mark.flaky(reruns=reruns, reruns_delay=1)
7+
@pytest.mark.parametrize("ex_app_path", get_apps("tests/playwright/deploys"))
8+
def test_deploy_examples(page: Page, ex_app_path: str) -> None:
9+
validate_example(page, ex_app_path)

0 commit comments

Comments
 (0)