Skip to content

Commit

Permalink
Add user journey test for canceling a build (#812)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
peytondmurray and pre-commit-ci[bot] authored Apr 22, 2024
1 parent 3c4547b commit 71a5632
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: complicated-environment
channels:
- conda-forge
- bokeh
dependencies:
- python=3.10
- panel
- ipykernel
- ipywidgets
- ipywidgets_bokeh
- holoviews
- openjdk=17.0.9
- pyspark
- findspark
- jhsingle-native-proxy>=0.8.2
- bokeh-root-cmd>=0.1.2
- nbconvert
- pip:
- nrtk==0.3.0
- xaitk-saliency==0.7.0
- maite==0.5.0
- daml==0.44.5
- hypothesis >=6.61.0,<7.0.0
- pytest >=7.2.0,<8.0
- pytest-cov >=4.0.0,<5.0
- pytest-mock >= 3.10.0,<4.0
- pytest-snapshot >= 0.9.0
- pytest-xdist >=3.3.1,<4.0.0
- types-python-dateutil >=2.8.19,<3.0.0
- tox >=4.6.4,<5.0.0
- virtualenv-pyenv >=0.3.0,<1.0.0
- jupytext >= 1.14.0
- numpydoc >= 1.5.0
- pyright >= 1.1.280
- loguru
- torch>=2.1
- torchmetrics
- torchvision
- multiprocess
- keras
- yolov5
- smqtk-detection[centernet]
32 changes: 32 additions & 0 deletions conda-store-server/tests/user_journeys/test_user_journeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,35 @@ def test_failed_build_logs(base_url: str):
namespace,
build_request["data"]["specification"]["name"],
)


@pytest.mark.user_journey
def test_cancel_build(base_url: str):
"""Test that a user cancel a build in progress."""
api = utils.API(base_url=base_url)
namespace = "default"
build_id = api.create_environment(
namespace,
"tests/user_journeys/test_data/complicated_environment.yaml",
wait=False,
).json()["data"]["build_id"]

assert api.get_build_status(build_id) in [
utils.BuildStatus.QUEUED,
utils.BuildStatus.BUILDING,
]
api.cancel_build(build_id)

def check_status():
status = api.get_build_status(build_id)
if status in [utils.BuildStatus.QUEUED, utils.BuildStatus.BUILDING]:
return False

if status in [utils.BuildStatus.COMPLETED, utils.BuildStatus.FAILED]:
raise ValueError(
f"Build {build_id} {status.value.lower()}, but should have been canceled."
)

return status == utils.BuildStatus.CANCELED

utils.wait_for_condition(check_status, timeout=60, interval=1)
117 changes: 81 additions & 36 deletions conda-store-server/tests/user_journeys/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import uuid

from enum import Enum
from typing import Any, Optional, Union
from typing import Any, Callable, Optional, Union

import requests
import utils.time_utils as time_utils
Expand Down Expand Up @@ -94,22 +94,13 @@ def _login(self, username: str, password: str) -> None:
data = token_response.json()
self.token = data["data"]["token"]

def create_namespace(
self,
namespace: Union[str, None] = None,
max_iterations: int = 100,
sleep_time: int = 5,
) -> requests.Response:
def create_namespace(self, namespace: Union[str, None] = None) -> requests.Response:
"""Create a namespace.
Parameters
----------
namespace : str
Name of the namespace to create. If None, use a random namespace name
max_iterations : int
Max number of times to check whether the namespace was created before failing
sleep_time : int
Seconds to wait between each status check
Returns
-------
Expand All @@ -120,18 +111,7 @@ def create_namespace(
namespace = self.gen_random_namespace()

self._make_request(f"api/v1/namespace/{namespace}", method="POST")
for i in range(max_iterations):
response = self._make_request(f"api/v1/namespace/{namespace}")
status = NamespaceStatus(response.json()["status"])
if status in [NamespaceStatus.OK, NamespaceStatus.ERROR]:
return response

time.sleep(sleep_time)

raise TimeoutError(
f"Timed out waiting to create namespace {namespace}. Current response: "
f"{response.json()}"
)
return self._make_request(f"api/v1/namespace/{namespace}")

def create_token(
self, namespace: str, role: str, default_namespace: str = "default"
Expand All @@ -150,6 +130,7 @@ def create_environment(
specification_path: str,
max_iterations: int = 100,
sleep_time: int = 5,
wait: bool = True,
) -> requests.Response:
"""Create an environment.
Expand All @@ -163,6 +144,10 @@ def create_environment(
Max number of times to check whether the build completed before failing
sleep_time : int
Seconds to wait between each status check
wait : bool
If True, wait for the build to complete, fail, or be canceled before
returning a response. If False, return the response from the specification
POST immediately without waiting
Returns
-------
Expand All @@ -178,24 +163,23 @@ def create_environment(
method="POST",
json_data={"namespace": namespace, "specification": specification_content},
)
if not wait:
return response

build_id = response.json()["data"]["build_id"]
for i in range(max_iterations):
response = self._make_request(f"api/v1/build/{build_id}/")
status = BuildStatus(response.json()["data"]["status"])

if status in [
BuildStatus.COMPLETED,
def check_status():
status = self.get_build_status(build_id)
if status in [BuildStatus.QUEUED, BuildStatus.BUILDING]:
return False
return status in [
BuildStatus.FAILED,
BuildStatus.CANCELED,
]:
return response

time.sleep(sleep_time)
BuildStatus.COMPLETED,
]

raise TimeoutError(
f"Timed out waiting to create namespace {namespace}. Current response: "
f"{response.json()}"
)
wait_for_condition(check_status, timeout=120, interval=1)
return self._make_request(f"api/v1/build/{build_id}/")

def delete_environment(
self, namespace: str, environment_name: str
Expand Down Expand Up @@ -309,3 +293,64 @@ def get_environment(self, namespace: str, environment: str) -> dict[str, Any]:
return self._make_request(
f"api/v1/environment/{namespace}/{environment}/"
).json()["data"]

def cancel_build(self, build_id: int) -> requests.Response:
"""Cancel a build in progress.
Parameters
----------
build_id : int
ID of the build to cancel
Returns
-------
requests.Response
Response from the server
"""
return self._make_request(f"api/v1/build/{build_id}/cancel/", method="PUT")

def get_build_status(self, build_id: int) -> BuildStatus:
"""Get the status of a build as a BuildStatus instance.
Parameters
----------
build_id : int
ID of the build to get the status for
Returns
-------
BuildStatus
Build status for the given build ID
"""
response = self._make_request(f"api/v1/build/{build_id}/")
return BuildStatus(response.json()["data"]["status"])


def wait_for_condition(
condition: Callable[[], bool], timeout: int = 60, interval: int = 1
):
"""Call `condition` until it returns `True`.
`condition` will be called every `interval` seconds up to a maximum of `timeout`
seconds, at which point a ValueError is raised.
Parameters
----------
condition : Callable[[], bool]
Function to call until True is returned
timeout : int
Number of seconds to continue calling `condition` for before timing out
interval : int
Number of seconds between consecutive calls
"""
initial_time = time.time()
while time.time() - initial_time < timeout:
result = condition()
if result:
return

time.sleep(interval)

raise ValueError(
f"Timeout after {timeout}s waiting for condition. Last result: {result}"
)

0 comments on commit 71a5632

Please sign in to comment.