Skip to content

Commit 8e957ff

Browse files
fix: Add new cleanup fixture to track docker assets that need to be cleaned up (#129)
#### Relevant issue or PR #34 #### Description of changes Add cleanup fixture to clean up images, containers, and projects generated by the unit tests. Also added cleanup to the test_examples images since they used a different image build with no cleanup. #### Testing done Unit testing + manual testing to check if any projects and images exists after running tests since assert functions are frowned upon in fixtures. Added keyboard interrupt into serve_pipline end to end test ``` tests/endtoend_tests/test_endtoend.py::test_tesseract_serve_pipeline AKOAKO cleaning up! {'images': ['tmp_tesseract_image_jpo5rz28xc2xte0f'], 'project_ids': ['tesseract-o7f82iixoszg'], 'containers': []} !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! KeyboardInterrupt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! /Users/ako/work/tesseract-core/tests/endtoend_tests/test_endtoend.py:188: KeyboardInterrupt (to show a full traceback on KeyboardInterrupt use --full-trace) ================================================ no tests ran in 41.00s ================================================ ``` You can see that the cleanup fixture still runs and `ps` `list` all are a clean slate. #### License - [x ] By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license](https://pasteurlabs.github.io/tesseract/LICENSE). - [ x] I sign the Developer Certificate of Origin below by adding my name and email address to the `Signed-off-by` line. <details> <summary><b>Developer Certificate of Origin</b></summary> ```text Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` </details> Signed-off-by: Angela Ko [email protected] --------- Co-authored-by: Dion Häfner <[email protected]>
1 parent 3ef0726 commit 8e957ff

File tree

5 files changed

+194
-187
lines changed

5 files changed

+194
-187
lines changed

tests/conftest.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -187,36 +187,55 @@ def docker_client():
187187
return docker_client_module.CLIDockerClient()
188188

189189

190+
@pytest.fixture(scope="module")
191+
def docker_cleanup(docker_client, request):
192+
"""Clean up all tesseracts created by the tests."""
193+
from tesseract_core.sdk.docker_client import ContainerError, ImageNotFound
194+
195+
# Shared object to track what objects need to be cleaned up in each test
196+
context = {"images": [], "project_ids": [], "containers": []}
197+
198+
def cleanup_func():
199+
# Teardown projects first
200+
for project_id in context["project_ids"]:
201+
if docker_client.compose.exists(project_id):
202+
docker_client.compose.down(project_id)
203+
204+
# Remove containers
205+
for container in context["containers"]:
206+
try:
207+
container_obj = docker_client.containers.get(container)
208+
except ContainerError:
209+
continue
210+
container_obj.remove(v=True, force=True)
211+
212+
# Remove images
213+
for image in context["images"]:
214+
try:
215+
docker_client.images.remove(image)
216+
except ImageNotFound:
217+
continue
218+
219+
request.addfinalizer(cleanup_func)
220+
return context
221+
222+
190223
@pytest.fixture
191-
def dummy_image_name(docker_client):
224+
def dummy_image_name(docker_cleanup):
192225
"""Create a dummy image name, and clean up after the test."""
193-
from tesseract_core.sdk.docker_client import ImageNotFound
194-
195226
image_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
196227
image_name = f"tmp_tesseract_image_{image_id}"
197-
try:
198-
yield image_name
199-
finally:
200-
try:
201-
docker_client.images.remove(image_name)
202-
except ImageNotFound:
203-
pass
228+
docker_cleanup["images"].append(image_name)
229+
yield image_name
204230

205231

206232
@pytest.fixture(scope="module")
207-
def shared_dummy_image_name(docker_client):
233+
def shared_dummy_image_name(docker_cleanup):
208234
"""Create a dummy image name, and clean up after all tests."""
209-
from tesseract_core.sdk.docker_client import ImageNotFound
210-
211235
image_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
212236
image_name = f"tmp_tesseract_image_{image_id}"
213-
try:
214-
yield image_name
215-
finally:
216-
try:
217-
docker_client.images.remove(image_name)
218-
except ImageNotFound:
219-
pass
237+
docker_cleanup["images"].append(image_name)
238+
yield image_name
220239

221240

222241
@pytest.fixture

tests/endtoend_tests/common.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from typer.testing import CliRunner
99

1010
from tesseract_core.sdk.cli import app
11-
from tesseract_core.sdk.docker_client import CLIDockerClient, ImageNotFound
11+
from tesseract_core.sdk.docker_client import (
12+
CLIDockerClient,
13+
ContainerError,
14+
ImageNotFound,
15+
)
1216

1317

1418
def image_exists(client, image_name, tesseract_only: bool = True):
@@ -25,6 +29,15 @@ def image_exists(client, image_name, tesseract_only: bool = True):
2529
return False
2630

2731

32+
def container_exists(client, container_name_or_id, tesseract_only: bool = True):
33+
"""Checks if containers exists."""
34+
try:
35+
client.containers.get(container_name_or_id, tesseract_only)
36+
return True
37+
except ContainerError:
38+
return False
39+
40+
2841
def print_debug_info(result):
2942
"""Print debug info from result of a CLI command if it failed."""
3043
if result.exit_code == 0:
@@ -75,4 +88,5 @@ def build_tesseract(
7588

7689
image_tags = json.loads(result.stdout.strip())
7790
assert image_name in image_tags
91+
7892
return image_tags[0]

tests/endtoend_tests/test_endtoend.py

Lines changed: 58 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717

1818
@pytest.fixture(scope="module")
19-
def built_image_name(docker_client, shared_dummy_image_name, dummy_tesseract_location):
19+
def built_image_name(
20+
docker_client, docker_cleanup, shared_dummy_image_name, dummy_tesseract_location
21+
):
2022
"""Build the dummy Tesseract image for the tests."""
2123
image_name = build_tesseract(dummy_tesseract_location, shared_dummy_image_name)
2224
assert image_exists(docker_client, image_name)
25+
docker_cleanup["images"].append(image_name)
2326
yield image_name
2427

2528

@@ -34,7 +37,7 @@ def built_image_name(docker_client, shared_dummy_image_name, dummy_tesseract_loc
3437

3538
@pytest.mark.parametrize("tag,recipe,base_image", build_matrix)
3639
def test_build_from_init_endtoend(
37-
docker_client, dummy_image_name, tmp_path, tag, recipe, base_image
40+
docker_client, docker_cleanup, dummy_image_name, tmp_path, tag, recipe, base_image
3841
):
3942
"""Test that a trivial (empty) Tesseract image can be built from init."""
4043
cli_runner = CliRunner(mix_stderr=False)
@@ -59,6 +62,7 @@ def test_build_from_init_endtoend(
5962
tmp_path, dummy_image_name, config_override=config_override, tag=img_tag
6063
)
6164
assert image_exists(docker_client, image_name)
65+
docker_cleanup["images"].append(image_name)
6266

6367
# Test that the image can be run and that --help is forwarded correctly
6468
result = cli_runner.invoke(
@@ -143,59 +147,48 @@ def test_tesseract_run_stdout(built_image_name):
143147
raise
144148

145149

146-
def test_tesseract_serve_pipeline(docker_client, built_image_name):
150+
def test_tesseract_serve_pipeline(docker_client, built_image_name, docker_cleanup):
147151
cli_runner = CliRunner(mix_stderr=False)
148152
project_id = None
149-
try:
150-
run_res = cli_runner.invoke(
151-
app,
152-
[
153-
"serve",
154-
built_image_name,
155-
],
156-
catch_exceptions=False,
157-
)
153+
run_res = cli_runner.invoke(
154+
app,
155+
[
156+
"serve",
157+
built_image_name,
158+
],
159+
catch_exceptions=False,
160+
)
158161

159-
assert run_res.exit_code == 0, run_res.stderr
160-
assert run_res.stdout
162+
assert run_res.exit_code == 0, run_res.stderr
163+
assert run_res.stdout
161164

162-
project_meta = json.loads(run_res.stdout)
165+
project_meta = json.loads(run_res.stdout)
163166

164-
project_id = project_meta["project_id"]
165-
project_containers = project_meta["containers"][0]["name"]
166-
if not project_containers:
167-
raise ValueError(f"Could not find container for project '{project_id}'")
167+
project_id = project_meta["project_id"]
168+
docker_cleanup["project_ids"].append(project_id)
169+
project_containers = project_meta["containers"][0]["name"]
170+
if not project_containers:
171+
raise ValueError(f"Could not find container for project '{project_id}'")
168172

169-
project_container = docker_client.containers.get(project_containers)
170-
assert project_container.name == project_meta["containers"][0]["name"]
171-
assert project_container.host_port == project_meta["containers"][0]["port"]
173+
project_container = docker_client.containers.get(project_containers)
174+
assert project_container.name == project_meta["containers"][0]["name"]
175+
assert project_container.host_port == project_meta["containers"][0]["port"]
172176

173-
# Ensure served Tesseract is usable
174-
res = requests.get(f"http://localhost:{project_container.host_port}/health")
175-
assert res.status_code == 200, res.text
177+
# Ensure served Tesseract is usable
178+
res = requests.get(f"http://localhost:{project_container.host_port}/health")
179+
assert res.status_code == 200, res.text
176180

177-
# Ensure project id is shown in `tesseract ps`
178-
run_res = cli_runner.invoke(
179-
app,
180-
["ps"],
181-
env={"COLUMNS": "1000"},
182-
catch_exceptions=False,
183-
)
184-
assert run_res.exit_code == 0, run_res.stderr
185-
assert project_id in run_res.stdout
186-
assert project_container.host_port in run_res.stdout
187-
assert project_container.short_id in run_res.stdout
188-
finally:
189-
if project_id:
190-
run_res = cli_runner.invoke(
191-
app,
192-
[
193-
"teardown",
194-
project_id,
195-
],
196-
catch_exceptions=False,
197-
)
198-
assert run_res.exit_code == 0, run_res.stderr
181+
# Ensure project id is shown in `tesseract ps`
182+
run_res = cli_runner.invoke(
183+
app,
184+
["ps"],
185+
env={"COLUMNS": "1000"},
186+
catch_exceptions=False,
187+
)
188+
assert run_res.exit_code == 0, run_res.stderr
189+
assert project_id in run_res.stdout
190+
assert project_container.host_port in run_res.stdout
191+
assert project_container.short_id in run_res.stdout
199192

200193

201194
@pytest.mark.parametrize("tear_all", [True, False])
@@ -320,7 +313,7 @@ def test_tesseract_serve_ports_error(built_image_name):
320313

321314

322315
@pytest.mark.parametrize("port", ["34567", "34567-34569"])
323-
def test_tesseract_serve_ports(built_image_name, port):
316+
def test_tesseract_serve_ports(built_image_name, port, docker_cleanup):
324317
"""Try to serve multiple Tesseracts on multiple ports."""
325318
cli_runner = CliRunner(mix_stderr=False)
326319
project_id = None
@@ -336,39 +329,27 @@ def test_tesseract_serve_ports(built_image_name, port):
336329

337330
project_meta = json.loads(run_res.stdout)
338331
project_id = project_meta["project_id"]
332+
docker_cleanup["project_ids"].append(project_id)
339333

340-
# Wrap test in try-finally to ensure teardown of served Tesseract.
341-
try:
342-
# Ensure that actual used ports are in the specified port range.
343-
test_ports = port.split("-")
344-
start_port = int(test_ports[0])
345-
end_port = int(test_ports[1]) if len(test_ports) > 1 else start_port
334+
# Ensure that actual used ports are in the specified port range.
335+
test_ports = port.split("-")
336+
start_port = int(test_ports[0])
337+
end_port = int(test_ports[1]) if len(test_ports) > 1 else start_port
346338

347-
port = int(project_meta["containers"][0]["port"])
348-
assert port in range(start_port, end_port + 1)
339+
port = int(project_meta["containers"][0]["port"])
340+
assert port in range(start_port, end_port + 1)
349341

350-
# Ensure specified ports are in `tesseract ps` and served Tesseracts are usable.
351-
run_res = cli_runner.invoke(
352-
app,
353-
["ps"],
354-
env={"COLUMNS": "1000"},
355-
catch_exceptions=False,
356-
)
342+
# Ensure specified ports are in `tesseract ps` and served Tesseracts are usable.
343+
run_res = cli_runner.invoke(
344+
app,
345+
["ps"],
346+
env={"COLUMNS": "1000"},
347+
catch_exceptions=False,
348+
)
357349

358-
res = requests.get(f"http://localhost:{port}/health")
359-
assert res.status_code == 200, res.text
360-
assert str(port) in run_res.stdout
361-
finally:
362-
if project_id:
363-
run_res = cli_runner.invoke(
364-
app,
365-
[
366-
"teardown",
367-
project_id,
368-
],
369-
catch_exceptions=False,
370-
)
371-
assert run_res.exit_code == 0, run_res.stderr
350+
res = requests.get(f"http://localhost:{port}/health")
351+
assert res.status_code == 200, res.text
352+
assert str(port) in run_res.stdout
372353

373354

374355
def test_tesseract_serve_with_volumes(built_image_name, tmp_path, docker_client):

0 commit comments

Comments
 (0)