Skip to content

Commit 9bb3039

Browse files
Merge pull request #71 from streamlit/STREAMLIT-4007-script-to-check-if-e2e-setup-is-identical
2 parents 3abafe8 + e98625b commit 9bb3039

File tree

10 files changed

+298
-49
lines changed

10 files changed

+298
-49
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ jobs:
5151
- name: Check dependencies for examples
5252
run: ./dev.py examples-check-deps
5353

54+
- name: Check e2e utils files
55+
run: ./dev.py e2e-utils-check
56+
5457
- name: Checkout streamlit/streamlit
5558
if: matrix.component_lib_version == 'develop'
5659
uses: actions/checkout@v3

cookiecutter/{{ cookiecutter.package_name }}/e2e/e2e_utils.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818

1919
def _find_free_port():
20+
"""Find and return a free port on the local machine."""
2021
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
2122
s.bind(("", 0)) # 0 means that the OS chooses a random port
2223
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -26,14 +27,22 @@ def _find_free_port():
2627
class AsyncSubprocess:
2728
"""A context manager. Wraps subprocess. Popen to capture output safely."""
2829

29-
def __init__(self, args, cwd=None, env=None):
30+
def __init__(self, args: typing.List[str], cwd: typing.Optional[str] = None,
31+
env: typing.Optional[typing.Dict[str, str]] = None):
32+
"""Initialize an AsyncSubprocess instance.
33+
34+
Args:
35+
args (List[str]): List of command-line arguments.
36+
cwd (str, optional): Current working directory. Defaults to None.
37+
env (dict, optional): Environment variables. Defaults to None.
38+
"""
3039
self.args = args
3140
self.cwd = cwd
3241
self.env = env
3342
self._proc = None
3443
self._stdout_file = None
3544

36-
def terminate(self):
45+
def terminate(self) -> typing.Optional[str]:
3746
"""Terminate the process and return its stdout/stderr in a string."""
3847
if self._proc is not None:
3948
self._proc.terminate()
@@ -50,11 +59,13 @@ def terminate(self):
5059

5160
return stdout
5261

53-
def __enter__(self):
62+
def __enter__(self) -> "AsyncSubprocess":
63+
"""Start the subprocess when entering the context."""
5464
self.start()
5565
return self
5666

5767
def __exit__(self, exc_type, exc_val, exc_tb):
68+
"""Stop the subprocess and close resources when exiting the context."""
5869
self.stop()
5970

6071
def start(self):
@@ -74,6 +85,7 @@ def start(self):
7485
)
7586

7687
def stop(self):
88+
"""Terminate the subprocess and close resources."""
7789
if self._proc is not None:
7890
self._proc.terminate()
7991
self._proc = None
@@ -83,21 +95,32 @@ def stop(self):
8395

8496

8597
class StreamlitRunner:
98+
"""A context manager for running Streamlit scripts."""
99+
86100
def __init__(
87101
self, script_path: os.PathLike, server_port: typing.Optional[int] = None
88102
):
103+
"""Initialize a StreamlitRunner instance.
104+
105+
Args:
106+
script_path (os.PathLike): Path to the Streamlit script to run.
107+
server_port (int, optional): Port for the Streamlit server. Defaults to None.
108+
"""
89109
self._process = None
90110
self.server_port = server_port
91111
self.script_path = script_path
92112

93-
def __enter__(self):
113+
def __enter__(self) -> "StreamlitRunner":
114+
"""Start the Streamlit server when entering the context."""
94115
self.start()
95116
return self
96117

97118
def __exit__(self, type, value, traceback):
119+
"""Stop the Streamlit server and close resources when exiting the context."""
98120
self.stop()
99121

100122
def start(self):
123+
"""Start the Streamlit server using the specified script and options."""
101124
self.server_port = self.server_port or _find_free_port()
102125
self._process = AsyncSubprocess(
103126
[
@@ -118,12 +141,20 @@ def start(self):
118141
raise RuntimeError("Application failed to start")
119142

120143
def stop(self):
144+
"""Stop the Streamlit server and close resources."""
121145
self._process.stop()
122146

123-
def is_server_running(self, timeout: int = 30):
147+
def is_server_running(self, timeout: int = 30) -> bool:
148+
"""Check if the Streamlit server is running.
149+
150+
Args:
151+
timeout (int, optional): Maximum time to wait for the server to start. Defaults to 30.
152+
153+
Returns:
154+
bool: True if the server is running, False otherwise.
155+
"""
124156
with requests.Session() as http_session:
125157
start_time = time.time()
126-
print("Start loop: ", start_time)
127158
while True:
128159
with contextlib.suppress(requests.RequestException):
129160
response = http_session.get(self.server_url + "/_stcore/health")
@@ -134,7 +165,8 @@ def is_server_running(self, timeout: int = 30):
134165
return False
135166

136167
@property
137-
def server_url(self):
168+
def server_url(self) -> str:
169+
"""Get the URL of the Streamlit server."""
138170
if not self.server_port:
139171
raise RuntimeError("Unknown server port")
140172
return f"http://localhost:{self.server_port}"

dev.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
To list the available commands, run ./dev.py --help.
77
"""
88
import argparse
9+
import glob
910
import tempfile
1011
import typing
1112
import shlex
@@ -169,6 +170,26 @@ def cmd_example_check_deps(args):
169170
sys.exit(exit_code)
170171

171172

173+
def cmd_check_test_utils(args):
174+
"""Check that e2e utils files are identical"""
175+
file_list = glob.glob('**/e2e_utils.py', recursive=True)
176+
if file_list:
177+
reference_file = file_list[0]
178+
else:
179+
print("Cannot find e2e_utils.py files")
180+
sys.exit(1)
181+
182+
for file_path in file_list:
183+
run_verbose([
184+
"git",
185+
"--no-pager",
186+
"diff",
187+
"--no-index",
188+
str(reference_file),
189+
str(file_path),
190+
])
191+
192+
172193
class CookiecutterVariant(typing.NamedTuple):
173194
replay_file: Path
174195
repo_directory: Path
@@ -270,6 +291,7 @@ def cmd_update_templates(args):
270291
"examples-check-deps": cmd_example_check_deps,
271292
"templates-check-not-modified": cmd_check_templates_using_cookiecutter,
272293
"templates-update": cmd_update_templates,
294+
"e2e-utils-check": cmd_check_test_utils,
273295
"e2e-build-images": cmd_e2e_build_images,
274296
"e2e-run-tests": cmd_e2e_run,
275297
"docker-images-cleanup": cmd_docker_images_cleanup

examples/CustomDataframe/e2e/e2e_utils.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818

1919
def _find_free_port():
20+
"""Find and return a free port on the local machine."""
2021
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
2122
s.bind(("", 0)) # 0 means that the OS chooses a random port
2223
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -26,14 +27,22 @@ def _find_free_port():
2627
class AsyncSubprocess:
2728
"""A context manager. Wraps subprocess. Popen to capture output safely."""
2829

29-
def __init__(self, args, cwd=None, env=None):
30+
def __init__(self, args: typing.List[str], cwd: typing.Optional[str] = None,
31+
env: typing.Optional[typing.Dict[str, str]] = None):
32+
"""Initialize an AsyncSubprocess instance.
33+
34+
Args:
35+
args (List[str]): List of command-line arguments.
36+
cwd (str, optional): Current working directory. Defaults to None.
37+
env (dict, optional): Environment variables. Defaults to None.
38+
"""
3039
self.args = args
3140
self.cwd = cwd
3241
self.env = env
3342
self._proc = None
3443
self._stdout_file = None
3544

36-
def terminate(self):
45+
def terminate(self) -> typing.Optional[str]:
3746
"""Terminate the process and return its stdout/stderr in a string."""
3847
if self._proc is not None:
3948
self._proc.terminate()
@@ -50,11 +59,13 @@ def terminate(self):
5059

5160
return stdout
5261

53-
def __enter__(self):
62+
def __enter__(self) -> "AsyncSubprocess":
63+
"""Start the subprocess when entering the context."""
5464
self.start()
5565
return self
5666

5767
def __exit__(self, exc_type, exc_val, exc_tb):
68+
"""Stop the subprocess and close resources when exiting the context."""
5869
self.stop()
5970

6071
def start(self):
@@ -74,6 +85,7 @@ def start(self):
7485
)
7586

7687
def stop(self):
88+
"""Terminate the subprocess and close resources."""
7789
if self._proc is not None:
7890
self._proc.terminate()
7991
self._proc = None
@@ -83,21 +95,32 @@ def stop(self):
8395

8496

8597
class StreamlitRunner:
98+
"""A context manager for running Streamlit scripts."""
99+
86100
def __init__(
87101
self, script_path: os.PathLike, server_port: typing.Optional[int] = None
88102
):
103+
"""Initialize a StreamlitRunner instance.
104+
105+
Args:
106+
script_path (os.PathLike): Path to the Streamlit script to run.
107+
server_port (int, optional): Port for the Streamlit server. Defaults to None.
108+
"""
89109
self._process = None
90110
self.server_port = server_port
91111
self.script_path = script_path
92112

93-
def __enter__(self):
113+
def __enter__(self) -> "StreamlitRunner":
114+
"""Start the Streamlit server when entering the context."""
94115
self.start()
95116
return self
96117

97118
def __exit__(self, type, value, traceback):
119+
"""Stop the Streamlit server and close resources when exiting the context."""
98120
self.stop()
99121

100122
def start(self):
123+
"""Start the Streamlit server using the specified script and options."""
101124
self.server_port = self.server_port or _find_free_port()
102125
self._process = AsyncSubprocess(
103126
[
@@ -118,12 +141,20 @@ def start(self):
118141
raise RuntimeError("Application failed to start")
119142

120143
def stop(self):
144+
"""Stop the Streamlit server and close resources."""
121145
self._process.stop()
122146

123-
def is_server_running(self, timeout: int = 30):
147+
def is_server_running(self, timeout: int = 30) -> bool:
148+
"""Check if the Streamlit server is running.
149+
150+
Args:
151+
timeout (int, optional): Maximum time to wait for the server to start. Defaults to 30.
152+
153+
Returns:
154+
bool: True if the server is running, False otherwise.
155+
"""
124156
with requests.Session() as http_session:
125157
start_time = time.time()
126-
print("Start loop: ", start_time)
127158
while True:
128159
with contextlib.suppress(requests.RequestException):
129160
response = http_session.get(self.server_url + "/_stcore/health")
@@ -134,7 +165,8 @@ def is_server_running(self, timeout: int = 30):
134165
return False
135166

136167
@property
137-
def server_url(self):
168+
def server_url(self) -> str:
169+
"""Get the URL of the Streamlit server."""
138170
if not self.server_port:
139171
raise RuntimeError("Unknown server port")
140172
return f"http://localhost:{self.server_port}"

0 commit comments

Comments
 (0)