Skip to content

Commit 3abafe8

Browse files
Setup running e2e tests in Docker image (#70)
1 parent 5c386b1 commit 3abafe8

File tree

4 files changed

+160
-58
lines changed

4 files changed

+160
-58
lines changed

.github/workflows/ci.yaml

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ on:
99
- "master"
1010

1111
jobs:
12-
build-examples-templates:
12+
build-examples-templates-and-run-dockerized-e2e-tests:
1313
runs-on: ubuntu-latest
1414

1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
node-version:
18+
node_version:
1919
# For details, see: https://nodejs.dev/en/about/releases/
2020
# Maintenance LTS. End Of Life: 2023-09-11
2121
- 16.x
@@ -26,18 +26,27 @@ jobs:
2626
component_lib_version:
2727
- current
2828
- develop
29+
streamlit_version:
30+
- latest
31+
- nightly
2932

30-
name: Examples + Templates / node-version=${{ matrix.node-version }} / component_lib_version=${{ matrix.component_lib_version }}
33+
env:
34+
NODE_VERSION: ${{ matrix.node_version }}
35+
PYTHON_VERSION: 3.8 # Oldest version supported by Streamlit
36+
STREAMLIT_VERSION: ${{ matrix.streamlit_version }}
37+
COMPONENT_LIB_VERSION: ${{ matrix.component_lib_version }}
38+
39+
name: Examples + Templates / node_version=${{ matrix.node_version }} / streamlit_version=${{ matrix.streamlit_version }} / component_lib_version=${{ matrix.component_lib_version }}
3140

3241
steps:
3342
- uses: actions/checkout@v3
3443
with:
3544
persist-credentials: false
3645

37-
- name: Use Node.js ${{ matrix.node-version }}
46+
- name: Use Node.js ${{ env.NODE_VERSION }}
3847
uses: actions/setup-node@v3
3948
with:
40-
node-version: ${{ matrix.node-version }}
49+
node-version: ${{ env.NODE_VERSION }}
4150

4251
- name: Check dependencies for examples
4352
run: ./dev.py examples-check-deps
@@ -98,17 +107,17 @@ jobs:
98107
path: dist/*.whl
99108
if-no-files-found: error
100109

101-
- name: Install python dependencies
102-
run: ./dev.py install-python-deps
103-
104-
- name: Install wheel packages
105-
run: ./dev.py install-wheel-packages
110+
- name: Set up Docker Buildx
111+
if: matrix.node_version == '19.x'
112+
uses: docker/setup-buildx-action@7703e82fbced3d0c9eec08dff4429c023a5fd9a9 # v2.9.1
106113

107-
- name: Install browsers
108-
run: ./dev.py install-browsers
114+
- name: Build docker images
115+
if: matrix.node_version == '19.x'
116+
run: ./dev.py e2e-build-images "--streamlit-version=${{ env.STREAMLIT_VERSION }}" "--python-version=${{ env.PYTHON_VERSION }}"
109117

110118
- name: Run e2e tests
111-
run: ./dev.py run-e2e
119+
if: matrix.node_version == '19.x'
120+
run: ./dev.py e2e-run-tests "--streamlit-version=${{ env.STREAMLIT_VERSION }}" "--python-version=${{ env.PYTHON_VERSION }}"
112121

113122
build-cookiecutter:
114123
runs-on: ubuntu-latest

Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# syntax=docker/dockerfile:1.4
2+
3+
ARG PYTHON_VERSION="3.11.4"
4+
FROM python:${PYTHON_VERSION}-slim-bullseye
5+
6+
SHELL ["/bin/bash", "-o", "pipefail", "-e", "-u", "-x", "-c"]
7+
8+
# Setup Pip
9+
ARG PIP_VERSION="23.2.1"
10+
ENV PIP_VERSION=${PIP_VERSION}
11+
12+
RUN pip install --no-cache-dir --upgrade "pip==${PIP_VERSION}" && pip --version
13+
14+
ENV PYTHONUNBUFFERED=1
15+
ENV PIP_ROOT_USER_ACTION=ignore
16+
RUN mkdir /component
17+
WORKDIR /component
18+
19+
# Install streamlit and components
20+
ARG STREAMLIT_VERSION="latest"
21+
ENV E2E_STREAMLIT_VERSION=${STREAMLIT_VERSION}
22+
23+
RUN <<"EOF"
24+
if [[ "${E2E_STREAMLIT_VERSION}" == "latest" ]]; then
25+
pip install --no-cache-dir "streamlit"
26+
elif [[ "${E2E_STREAMLIT_VERSION}" == "nightly" ]]; then
27+
pip uninstall --yes streamlit
28+
pip install --no-cache-dir "streamlit-nightly"
29+
else
30+
pip install --no-cache-dir "streamlit==${E2E_STREAMLIT_VERSION}"
31+
fi
32+
33+
# Coherence check
34+
installed_streamlit_version=$(python -c "import streamlit; print(streamlit.__version__)")
35+
echo "Installed Streamlit version: ${installed_streamlit_version}"
36+
if [[ "${E2E_STREAMLIT_VERSION}" == "nightly" ]]; then
37+
echo "${installed_streamlit_version}" | grep 'dev'
38+
else
39+
echo "${installed_streamlit_version}" | grep -v 'dev'
40+
fi
41+
EOF

dev.py

Lines changed: 96 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import json
1515
import sys
1616
import shutil
17+
import os
1718

1819
THIS_DIRECTORY = Path(__file__).parent.absolute()
1920
EXAMPLE_DIRECTORIES = [d for d in (THIS_DIRECTORY / 'examples').iterdir() if d.is_dir()]
@@ -35,60 +36,85 @@ def run_verbose(cmd_args, *args, **kwargs):
3536

3637
# Commands
3738
def cmd_all_npm_install(args):
38-
""""Install all node dependencies for all examples"""
39+
"""Install all node dependencies for all examples"""
3940
for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES:
4041
frontend_dir = next(project_dir.glob("*/frontend/"))
4142
run_verbose(["npm", "install"], cwd=str(frontend_dir))
4243

4344

4445
def cmd_all_npm_build(args):
45-
""""Build javascript code for all examples and templates"""
46+
"""Build javascript code for all examples and templates"""
4647
for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES:
4748
frontend_dir = next(project_dir.glob("*/frontend/"))
4849
run_verbose(["npm", "run", "build"], cwd=str(frontend_dir))
4950

5051

51-
def cmd_all_install_python_deps(args):
52-
""""Install all dependencies needed to run e2e tests for all examples and templates"""
52+
def cmd_e2e_build_images(args):
53+
"""Build docker images for each component e2e tests"""
5354
for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES:
54-
run_verbose(["pip", "install", "-e", ".[devel]"], cwd=str(project_dir))
55+
e2e_dir = next(project_dir.glob("**/e2e/"), None)
56+
if e2e_dir and os.listdir(e2e_dir):
57+
# Define the image tag for the docker image
58+
image_tag = (
59+
f"component-template:py-{args.python_version}-st-{args.streamlit_version}-component-{project_dir.parts[-1]}"
60+
)
61+
# Build the docker image with specified build arguments
62+
run_verbose(
63+
[
64+
"docker",
65+
"build",
66+
".",
67+
f"--build-arg=STREAMLIT_VERSION={args.streamlit_version}",
68+
f"--build-arg=PYTHON_VERSION={args.python_version}",
69+
f"--tag={image_tag}",
70+
"--progress=plain",
71+
],
72+
env={**os.environ, "DOCKER_BUILDKIT": "1"},
73+
)
5574

5675

57-
def cmd_all_install_wheel_packages(args):
58-
""""Install wheel packages of all examples and templates for e2e tests"""
76+
def cmd_e2e_run(args):
77+
"""Run e2e tests for all examples and templates in separate docker images"""
5978
for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES:
60-
wheel_files = list(project_dir.glob("dist/*.whl"))
61-
if wheel_files:
62-
wheel_file = wheel_files[0]
63-
run_verbose(["pip", "install", str(wheel_file)], cwd=str(project_dir))
64-
else:
65-
print(f"No wheel files found in {project_dir}")
66-
67-
68-
def cmd_install_browsers(args):
69-
""""Install multiple browsers to run e2e for all examples and templates"""
70-
run_verbose(["playwright", "install", "webkit", "chromium", "firefox", "--with-deps"])
71-
72-
73-
def cmd_all_run_e2e(args):
74-
""""Run e2e tests for all examples and templates"""
75-
for project_dir in TEMPLATE_DIRECTORIES:
79+
container_name = project_dir.parts[-1]
80+
image_tag = (
81+
f"component-template:py-{args.python_version}-st-{args.streamlit_version}-component-{container_name}"
82+
)
7683
e2e_dir = next(project_dir.glob("**/e2e/"), None)
77-
if e2e_dir:
78-
with tempfile.TemporaryDirectory() as tmp_dir:
79-
run_verbose(['python', '-m', 'venv', f"{tmp_dir}/venv"])
80-
wheel_files = list(project_dir.glob("dist/*.whl"))
81-
if wheel_files:
82-
wheel_file = wheel_files[0]
83-
run_verbose([f"{tmp_dir}/venv/bin/pip", "install", f"{str(wheel_file)}[devel]"], cwd=str(project_dir))
84-
else:
85-
print(f"No wheel files found in {project_dir}")
86-
run_verbose([f"{tmp_dir}/venv/bin/pytest", "-s", "--browser", "webkit", "--browser", "chromium", "--browser", "firefox", "--reruns", "5", str(e2e_dir)])
87-
88-
for project_dir in EXAMPLE_DIRECTORIES:
84+
if e2e_dir and os.listdir(e2e_dir):
85+
run_verbose([
86+
"docker",
87+
"run",
88+
"--tty",
89+
"--rm",
90+
"--name", container_name,
91+
"--volume", f"{e2e_dir.parent}/:/component/",
92+
image_tag,
93+
"/bin/sh", "-c", # Run a shell command inside the container
94+
"find /component/dist/ -name '*.whl' | xargs -I {} echo '{}[devel]' | xargs pip install && " # Install whl package and dev dependencies
95+
f"playwright install webkit chromium firefox --with-deps && " # Install browsers
96+
f"pytest", # Run pytest
97+
"-s",
98+
"--browser", "webkit",
99+
"--browser", "chromium",
100+
"--browser", "firefox",
101+
"--reruns", "5",
102+
"--capture=no",
103+
"--setup-show"
104+
])
105+
106+
107+
def cmd_docker_images_cleanup(args):
108+
"""Cleanup docker images and containers"""
109+
for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES:
110+
container_name = project_dir.parts[-1]
111+
image_name = (
112+
f"component-template:py-{args.python_version}-st-{args.streamlit_version}-component-{container_name}"
113+
)
89114
e2e_dir = next(project_dir.glob("**/e2e/"), None)
90-
if e2e_dir:
91-
run_verbose(["pytest", "-s", "--browser", "webkit", "--browser", "chromium", "--browser", "firefox", "--reruns", "5", str(e2e_dir)])
115+
if e2e_dir and os.listdir(e2e_dir):
116+
# Remove the associated Docker image
117+
run_verbose(["docker", "rmi", image_name])
92118

93119

94120
def cmd_all_python_build_package(args):
@@ -104,8 +130,8 @@ def cmd_all_python_build_package(args):
104130

105131
def check_deps(template_package_json, current_package_json):
106132
return (
107-
check_deps_section(template_package_json, current_package_json, 'dependencies') +
108-
check_deps_section(template_package_json, current_package_json, 'devDependencies')
133+
check_deps_section(template_package_json, current_package_json, 'dependencies') +
134+
check_deps_section(template_package_json, current_package_json, 'devDependencies')
109135
)
110136

111137

@@ -169,7 +195,8 @@ def cmd_check_templates_using_cookiecutter(args):
169195
replay_file_content = json.loads(cookiecutter_variant.replay_file.read_text())
170196

171197
with tempfile.TemporaryDirectory() as output_dir:
172-
print(f"Generating template with replay file: {cookiecutter_variant.replay_file.relative_to(THIS_DIRECTORY)}")
198+
print(
199+
f"Generating template with replay file: {cookiecutter_variant.replay_file.relative_to(THIS_DIRECTORY)}")
173200
run_verbose(
174201
[
175202
"cookiecutter",
@@ -243,10 +270,27 @@ def cmd_update_templates(args):
243270
"examples-check-deps": cmd_example_check_deps,
244271
"templates-check-not-modified": cmd_check_templates_using_cookiecutter,
245272
"templates-update": cmd_update_templates,
246-
"install-python-deps": cmd_all_install_python_deps,
247-
"install-wheel-packages": cmd_all_install_wheel_packages,
248-
"install-browsers": cmd_install_browsers,
249-
"run-e2e": cmd_all_run_e2e,
273+
"e2e-build-images": cmd_e2e_build_images,
274+
"e2e-run-tests": cmd_e2e_run,
275+
"docker-images-cleanup": cmd_docker_images_cleanup
276+
}
277+
278+
ARG_STREAMLIT_VERSION = ("--streamlit-version", "latest", "Streamlit version for which tests will be run.")
279+
ARG_PYTHON_VERSION = ("--python-version", os.environ.get("PYTHON_VERSION", "3.11.4"), "Python version for which tests will be run.")
280+
281+
ARGUMENTS = {
282+
"e2e-build-images": [
283+
ARG_STREAMLIT_VERSION,
284+
ARG_PYTHON_VERSION
285+
],
286+
"e2e-run-tests": [
287+
ARG_STREAMLIT_VERSION,
288+
ARG_PYTHON_VERSION
289+
],
290+
"docker-images-cleanup": [
291+
(*ARG_STREAMLIT_VERSION[:2], f"Streamlit version used to create the Docker resources"),
292+
(*ARG_PYTHON_VERSION[:2], f"Python version used to create the Docker resources")
293+
]
250294
}
251295

252296

@@ -256,7 +300,14 @@ def get_parser():
256300
subparsers = parser.add_subparsers(dest="subcommand", metavar="COMMAND")
257301
subparsers.required = True
258302
for command_name, command_fn in COMMANDS.items():
259-
subparsers.add_parser(command_name, help=command_fn.__doc__).set_defaults(func=command_fn)
303+
subparser = subparsers.add_parser(command_name, help=command_fn.__doc__)
304+
305+
if command_name in ARGUMENTS:
306+
for arg_name, arg_default, arg_help in ARGUMENTS[command_name]:
307+
subparser.add_argument(arg_name, default=arg_default, help=arg_help)
308+
309+
subparser.set_defaults(func=command_fn)
310+
260311
return parser
261312

262313

examples/RadioButton/e2e/test_radio_button.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
1010
BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "radio_button" / "example.py"
1111

12+
1213
@pytest.fixture(autouse=True, scope="module")
1314
def streamlit_app():
1415
with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner:

0 commit comments

Comments
 (0)