|
10 | 10 | import enapter |
11 | 11 |
|
12 | 12 | MOSQUITTO_PORT = "1883/tcp" |
| 13 | +# Timeout for Docker image pull operations (in seconds) |
| 14 | +DOCKER_PULL_TIMEOUT = 300 # 5 minutes |
13 | 15 |
|
14 | 16 |
|
15 | 17 | @pytest.fixture(name="enapter_mqtt_client") |
@@ -40,14 +42,12 @@ def fixture_mosquitto_container( |
40 | 42 | except docker.errors.ImageNotFound: |
41 | 43 | # Pull the image if not available locally |
42 | 44 | # This is handled by the CI workflow, but we keep it here for local testing |
43 | | - try: |
44 | | - docker_client.images.pull(image) |
45 | | - except docker.errors.APIError as e: |
46 | | - raise RuntimeError( |
47 | | - f"Failed to pull Docker image {image}. " |
48 | | - f"Please ensure Docker is running and you have network connectivity. " |
49 | | - f"Error: {e}" |
50 | | - ) from e |
| 45 | + pull_docker_image_with_timeout(docker_client, image) |
| 46 | + except docker.errors.APIError as e: |
| 47 | + raise RuntimeError( |
| 48 | + f"Failed to access Docker daemon or image {image}. " |
| 49 | + f"Please ensure Docker is running. Error: {e}" |
| 50 | + ) from e |
51 | 51 |
|
52 | 52 | try: |
53 | 53 | old_mosquitto = docker_client.containers.get(name) |
@@ -79,6 +79,46 @@ def random_unused_port() -> int: |
79 | 79 | return addr[1] |
80 | 80 |
|
81 | 81 |
|
| 82 | +def pull_docker_image_with_timeout( |
| 83 | + docker_client: docker.DockerClient, image: str, timeout: int = DOCKER_PULL_TIMEOUT |
| 84 | +) -> None: |
| 85 | + """Pull a Docker image with a timeout. |
| 86 | +
|
| 87 | + Args: |
| 88 | + docker_client: Docker client instance |
| 89 | + image: Image name to pull |
| 90 | + timeout: Timeout in seconds (default: DOCKER_PULL_TIMEOUT) |
| 91 | +
|
| 92 | + Raises: |
| 93 | + RuntimeError: If the image pull fails or times out |
| 94 | + TimeoutError: If the operation exceeds the timeout |
| 95 | + """ |
| 96 | + import concurrent.futures |
| 97 | + |
| 98 | + def _pull_image() -> None: |
| 99 | + try: |
| 100 | + docker_client.images.pull(image) |
| 101 | + except docker.errors.APIError as e: |
| 102 | + raise RuntimeError( |
| 103 | + f"Failed to pull Docker image {image}. " |
| 104 | + f"Please ensure Docker is running and you have network connectivity. " |
| 105 | + f"Error: {e}" |
| 106 | + ) from e |
| 107 | + |
| 108 | + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: |
| 109 | + future = executor.submit(_pull_image) |
| 110 | + try: |
| 111 | + future.result(timeout=timeout) |
| 112 | + except concurrent.futures.TimeoutError as e: |
| 113 | + raise TimeoutError( |
| 114 | + f"Timeout while pulling Docker image {image} after {timeout} seconds. " |
| 115 | + f"Please check your network connection or increase the timeout." |
| 116 | + ) from e |
| 117 | + except Exception: |
| 118 | + # Re-raise any other exceptions from the pull operation |
| 119 | + raise |
| 120 | + |
| 121 | + |
82 | 122 | @pytest.fixture(name="docker_client", scope="session") |
83 | 123 | def fixture_docker_client() -> Generator[docker.DockerClient, None, None]: |
84 | 124 | docker_client = docker.from_env() |
|
0 commit comments