23
from contextlib import contextmanager
24
from os.path import abspath, basename, dirname, join, splitext
25
from pathlib import Path
26
from tempfile import TemporaryFile
27
from typing import List
32
ROOT_DIR = dirname(dirname(abspath(__file__)))
33
FRONTEND_DIR = join(ROOT_DIR, "frontend")
35
CREDENTIALS_FILE = os.path.expanduser("~/.streamlit/credentials.toml")
38
class QuitException(BaseException):
43
"""A context manager. Wraps subprocess.Popen to capture output safely."""
45
def __init__(self, args, cwd=None, env=None):
50
self._stdout_file = None
53
"""Terminate the process and return its stdout/stderr in a string."""
55
if self._proc is not None:
56
self._proc.terminate()
62
if self._stdout_file is not None:
63
self._stdout_file.seek(0)
64
stdout = self._stdout_file.read()
65
self._stdout_file.close()
66
self._stdout_file = None
79
self._stdout_file = TemporaryFile("w+")
80
self._proc = subprocess.Popen(
83
stdout=self._stdout_file,
84
stderr=subprocess.STDOUT,
86
env={**os.environ.copy(), **self.env} if self.env else None,
89
def __exit__(self, exc_type, exc_val, exc_tb):
90
if self._proc is not None:
91
self._proc.terminate()
93
if self._stdout_file is not None:
94
self._stdout_file.close()
95
self._stdout_file = None
101
self.always_continue = False
103
self.record_results = False
105
self.update_snapshots = False
109
self.tests_dir_name = "e2e"
111
self.any_failed = False
113
self.cypress_env_vars = {}
116
def tests_dir(self) -> str:
117
return join(ROOT_DIR, self.tests_dir_name)
120
def cypress_flags(self) -> List[str]:
121
"""Flags to pass to Cypress"""
122
flags = ["--config", f"integrationFolder={self.tests_dir}/specs"]
123
if self.record_results:
124
flags.append("--record")
125
if self.update_snapshots:
126
flags.extend(["--env", "updateSnapshots=true"])
127
if self.cypress_env_vars:
128
vars_str = ",".join(f"{k}={v}" for k, v in self.cypress_env_vars.items())
129
flags.extend(["--env", vars_str])
133
def remove_if_exists(path):
134
"""Remove the given folder or file if it exists"""
135
if os.path.isfile(path):
137
elif os.path.isdir(path):
142
def move_aside_file(path):
143
"""Move a file aside if it exists; restore it on completion"""
145
if os.path.exists(path):
146
os.rename(path, f"{path}.bak")
153
os.rename(f"{path}.bak", path)
156
def create_credentials_toml(contents):
157
"""Writes ~/.streamlit/credentials.toml"""
158
os.makedirs(dirname(CREDENTIALS_FILE), exist_ok=True)
159
with open(CREDENTIALS_FILE, "w") as f:
163
def kill_with_pgrep(search_string):
164
result = subprocess.run(
165
f"pgrep -f '{search_string}'",
167
universal_newlines=True,
171
if result.returncode == 0:
172
for pid in result.stdout.split():
174
os.kill(int(pid), signal.SIGTERM)
175
except Exception as e:
176
print("Failed to kill process", e)
179
def kill_streamlits():
180
"""Kill any active `streamlit run` processes"""
181
kill_with_pgrep("streamlit run")
184
def kill_app_servers():
185
"""Kill any active app servers spawned by this script."""
186
kill_with_pgrep("running-streamlit-e2e-test")
192
streamlit_command: List[str],
193
show_output: bool = False,
195
"""Run a single e2e test.
197
An e2e test consists of a Streamlit script that produces a result, and
198
a Cypress test file that asserts that result is as expected.
203
The Context object that contains our global testing parameters.
205
The path of the Cypress spec file to run.
206
streamlit_command : list of str
207
The Streamlit command to run (passed directly to subprocess.Popen()).
212
True if the test succeeded.
224
with move_aside_file(CREDENTIALS_FILE):
225
create_credentials_toml('[general]\nemail="test@streamlit.io"')
228
while result not in (SUCCESS, SKIP, QUIT):
229
cypress_command = ["yarn", "cy:run", "--spec", specpath]
230
cypress_command.extend(ctx.cypress_flags)
233
f"{click.style('Running test:', fg='yellow', bold=True)}"
234
f"\n{click.style(' '.join(streamlit_command), fg='yellow')}"
235
f"\n{click.style(' '.join(cypress_command), fg='yellow')}"
239
with AsyncSubprocess(streamlit_command, cwd=FRONTEND_DIR) as streamlit_proc:
241
cypress_result = subprocess.run(
249
streamlit_stdout = streamlit_proc.terminate()
253
f"\n\n{click.style('Streamlit output:', fg='yellow', bold=True)}"
254
f"\n{streamlit_stdout}"
255
f"\n\n{click.style('Cypress output:', fg='yellow', bold=True)}"
256
f"\n{cypress_result.stdout}"
260
if cypress_result.returncode == 0:
262
click.echo(click.style("Success!\n", fg="green", bold=True))
268
click.echo(click.style("Failure!", fg="red", bold=True))
271
if ctx.always_continue:
275
user_input = click.prompt(
276
"[R]etry, [U]pdate snapshots, [S]kip, or [Q]uit?",
279
key = user_input[0].lower()
287
ctx.update_snapshots = True
293
if result != SUCCESS:
294
ctx.any_failed = True
297
raise QuitException()
299
return result == SUCCESS
302
def is_app_server_alive():
304
r = requests.get("http://localhost:3000/", timeout=3)
305
return r.status_code == requests.codes.ok
311
if is_app_server_alive():
312
print("Detected React app server already running, won't spawn a new one.")
317
"BUILD_AS_FAST_AS_POSSIBLE": "true",
318
"GENERATE_SOURCEMAP": "false",
319
"INLINE_RUNTIME_CHUNK": "false",
321
command = ["yarn", "start", "--running-streamlit-e2e-test"]
322
proc = AsyncSubprocess(command, cwd=FRONTEND_DIR, env=env)
324
print("Starting React app server...")
327
print("Waiting for React app server to come online...")
328
start_time = time.time()
329
while not is_app_server_alive():
333
if time.time() - start_time > 60 * 10:
335
"React app server seems to have had difficulty starting, exiting. Output:"
337
print(proc.terminate())
340
print("React app server is alive!")
346
"Run Streamlit e2e tests. If specific tests are specified, only those "
347
"tests will be run. If you don't specify specific tests, all tests "
352
"-a", "--always-continue", is_flag=True, help="Continue running on test failure."
358
help="Upload video results to the Cypress dashboard. "
359
"See https://docs.cypress.io/guides/dashboard/introduction.html for more details.",
363
"--update-snapshots",
365
help="Automatically update snapshots for failing tests.",
371
help="Run tests in 'e2e_flaky' instead of 'e2e'.",
377
help="Show Streamlit and Cypress output.",
379
@click.argument("tests", nargs=-1)
381
always_continue: bool,
382
record_results: bool,
383
update_snapshots: bool,
388
"""Run e2e tests. If any fail, exit with non-zero status."""
391
app_server = run_app_server()
394
remove_if_exists("frontend/test_results/cypress")
397
ctx.always_continue = always_continue
398
ctx.record_results = record_results
399
ctx.update_snapshots = update_snapshots
400
ctx.tests_dir_name = "e2e_flaky" if flaky_tests else "e2e"
403
p = Path(join(ROOT_DIR, ctx.tests_dir_name, "specs")).resolve()
405
paths = [Path(t).resolve() for t in tests]
407
paths = sorted(p.glob("*.spec.js"))
408
for spec_path in paths:
409
if basename(spec_path) == "st_hello.spec.js":
417
["streamlit", "hello", "--server.headless=false"],
423
["streamlit", "hello", "--server.headless=true"],
427
elif basename(spec_path) == "staticfiles_app.spec.js":
428
test_name, _ = splitext(basename(spec_path))
429
test_name, _ = splitext(test_name)
434
"streamlit_static_app.py",
436
if os.path.exists(test_path):
443
"--server.enableStaticServing=true",
448
elif basename(spec_path) == "hostframe.spec.js":
449
test_name, _ = splitext(basename(spec_path))
450
test_name, _ = splitext(test_name)
452
ctx.tests_dir, "scripts", "hostframe", "hostframe_app.py"
454
if os.path.exists(test_path):
467
test_name, _ = splitext(basename(spec_path))
468
test_name, _ = splitext(test_name)
469
test_path = join(ctx.tests_dir, "scripts", f"{test_name}.py")
470
if os.path.exists(test_path):
474
["streamlit", "run", test_path],
477
except QuitException:
482
app_server.terminate()
488
if __name__ == "__main__":