devservices 1.5.3__tar.gz → 1.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devservices-1.5.3 → devservices-1.5.4}/PKG-INFO +1 -1
- {devservices-1.5.3 → devservices-1.5.4}/README.md +1 -1
- {devservices-1.5.3 → devservices-1.5.4}/devservices/constants.py +7 -6
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/dependencies.py +2 -1
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/docker_compose.py +15 -32
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/install_binary.py +16 -5
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.5.3 → devservices-1.5.4}/pyproject.toml +1 -1
- {devservices-1.5.3 → devservices-1.5.4}/testing/utils.py +2 -2
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_docker_compose.py +51 -20
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_install_binary.py +56 -10
- {devservices-1.5.3 → devservices-1.5.4}/LICENSE.md +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/__init__.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/__init__.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/down.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/foreground.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/list_services.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/logs.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/purge.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/reset.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/serve.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/status.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/toggle.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/up.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/commands/update.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/configs/service_config.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/exceptions.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/main.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/__init__.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/console.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/devenv.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/docker.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/file_lock.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/git.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/github.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/retry.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/services.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/state.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices/utils/supervisor.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/SOURCES.txt +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/setup.cfg +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/testing/__init__.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/__init__.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_down.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_foreground.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_list_services.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_logs.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_purge.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_reset.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_serve.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_status.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_toggle.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_up.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/commands/test_update.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/configs/test_service_config.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/conftest.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_dependencies.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_docker.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_git.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_github.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_services.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_state.py +0 -0
- {devservices-1.5.3 → devservices-1.5.4}/tests/utils/test_supervisor.py +0 -0
|
@@ -33,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
33
33
|
The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
devservices==1.5.
|
|
36
|
+
devservices==1.5.4
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
### 2. Add devservices config files
|
|
@@ -22,7 +22,10 @@ class DependencyType(StrEnum):
|
|
|
22
22
|
SUPERVISOR = "supervisor"
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# Any installed Docker Compose at or above the minimum is used as is. The install
|
|
26
|
+
# version is only downloaded when Compose is missing or older than the minimum.
|
|
25
27
|
MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
|
|
28
|
+
DOCKER_COMPOSE_INSTALL_VERSION = "5.6.0"
|
|
26
29
|
DEVSERVICES_DIR_NAME = "devservices"
|
|
27
30
|
CONFIG_FILE_NAME = "config.yml"
|
|
28
31
|
DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker"))
|
|
@@ -42,15 +45,13 @@ DEVSERVICES_RELEASES_URL = (
|
|
|
42
45
|
"https://api.github.com/repos/getsentry/devservices/releases/latest"
|
|
43
46
|
)
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
# gsutil cp docker-compose-darwin-aarch64 gs://sentry-dev-infra-assets/docker-compose/v2.29.7/docker-compose-darwin-aarch64
|
|
47
|
-
# gsutil cp docker-compose-linux-x86_64 gs://sentry-dev-infra-assets/docker-compose/v2.29.7/docker-compose-linux-x86_64
|
|
48
|
-
DOCKER_COMPOSE_DOWNLOAD_URL = (
|
|
49
|
-
"https://storage.googleapis.com/sentry-dev-infra-assets/docker-compose"
|
|
50
|
-
)
|
|
48
|
+
DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
|
|
51
49
|
|
|
52
50
|
DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
|
|
53
51
|
BINARY_PERMISSIONS = 0o755
|
|
52
|
+
# Seconds a download may stall (connecting or between reads) before the attempt
|
|
53
|
+
# is abandoned and retried.
|
|
54
|
+
DOWNLOAD_TIMEOUT = 15
|
|
54
55
|
MAX_LOG_LINES = "100"
|
|
55
56
|
LOGGER_NAME = "devservices"
|
|
56
57
|
DOCKER_NETWORK_NAME = "devservices"
|
|
@@ -23,6 +23,7 @@ from devservices.constants import CONFIG_FILE_NAME
|
|
|
23
23
|
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
24
24
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
25
25
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
26
|
+
from devservices.constants import DOWNLOAD_TIMEOUT
|
|
26
27
|
from devservices.constants import DependencyType
|
|
27
28
|
from devservices.exceptions import ConfigNotFoundError
|
|
28
29
|
from devservices.exceptions import ConfigParseError
|
|
@@ -441,7 +442,7 @@ def _fetch_dependency(
|
|
|
441
442
|
|
|
442
443
|
def _download() -> bytes:
|
|
443
444
|
req = github.build_api_request(zip_url)
|
|
444
|
-
with urllib.request.urlopen(req) as response:
|
|
445
|
+
with urllib.request.urlopen(req, timeout=DOWNLOAD_TIMEOUT) as response:
|
|
445
446
|
return bytes(response.read())
|
|
446
447
|
|
|
447
448
|
try:
|
|
@@ -10,7 +10,6 @@ import shlex
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import time
|
|
12
12
|
from typing import NamedTuple
|
|
13
|
-
from typing import cast
|
|
14
13
|
|
|
15
14
|
from packaging import version
|
|
16
15
|
from sentry_sdk import logger as sentry_logger
|
|
@@ -19,6 +18,7 @@ from devservices.configs.service_config import load_service_config_from_file
|
|
|
19
18
|
from devservices.constants import CONFIG_FILE_NAME
|
|
20
19
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
21
20
|
from devservices.constants import DOCKER_COMPOSE_DOWNLOAD_URL
|
|
21
|
+
from devservices.constants import DOCKER_COMPOSE_INSTALL_VERSION
|
|
22
22
|
from devservices.constants import DOCKER_USER_PLUGIN_DIR
|
|
23
23
|
from devservices.constants import LOGGER_NAME
|
|
24
24
|
from devservices.constants import MINIMUM_DOCKER_COMPOSE_VERSION
|
|
@@ -69,7 +69,7 @@ def install_docker_compose() -> None:
|
|
|
69
69
|
else:
|
|
70
70
|
raise DockerComposeInstallationError(f"Unsupported operating system: {system}")
|
|
71
71
|
|
|
72
|
-
url = f"{DOCKER_COMPOSE_DOWNLOAD_URL}/v{
|
|
72
|
+
url = f"{DOCKER_COMPOSE_DOWNLOAD_URL}/v{DOCKER_COMPOSE_INSTALL_VERSION}/{binary_name_with_extension}"
|
|
73
73
|
destination = os.path.join(DOCKER_USER_PLUGIN_DIR, binary_name)
|
|
74
74
|
os.makedirs(DOCKER_USER_PLUGIN_DIR, exist_ok=True)
|
|
75
75
|
|
|
@@ -77,14 +77,14 @@ def install_docker_compose() -> None:
|
|
|
77
77
|
install_binary(
|
|
78
78
|
binary_name,
|
|
79
79
|
destination,
|
|
80
|
-
|
|
80
|
+
DOCKER_COMPOSE_INSTALL_VERSION,
|
|
81
81
|
url,
|
|
82
82
|
)
|
|
83
83
|
except BinaryInstallError as e:
|
|
84
84
|
raise DockerComposeInstallationError(f"Failed to install Docker Compose: {e}")
|
|
85
85
|
|
|
86
86
|
console.success(
|
|
87
|
-
f"Docker Compose {
|
|
87
|
+
f"Docker Compose {DOCKER_COMPOSE_INSTALL_VERSION} installed successfully to {destination}"
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
# Verify the installation
|
|
@@ -138,42 +138,25 @@ def check_docker_compose_version() -> None:
|
|
|
138
138
|
# Throw an error if docker daemon isn't running
|
|
139
139
|
check_docker_daemon_running()
|
|
140
140
|
try:
|
|
141
|
-
|
|
142
|
-
docker_compose_version = get_docker_compose_version()
|
|
141
|
+
version_output = get_docker_compose_version()
|
|
143
142
|
except DockerComposeError:
|
|
144
|
-
docker_compose_version = None
|
|
145
143
|
console.warning(
|
|
146
|
-
f"Docker Compose is not installed, attempting to install v{
|
|
144
|
+
f"Docker Compose is not installed, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}"
|
|
147
145
|
)
|
|
146
|
+
install_docker_compose()
|
|
147
|
+
return
|
|
148
148
|
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Use regex to find the version number
|
|
155
|
-
pattern = r"^(\d+\.\d+\.\d+)"
|
|
156
|
-
|
|
157
|
-
match = re.search(pattern, version_output)
|
|
158
|
-
if match:
|
|
159
|
-
# There is a chance that Any type is returned, so cast this
|
|
160
|
-
docker_compose_version = cast(str, match.group(1))
|
|
161
|
-
else:
|
|
162
|
-
docker_compose_version = None
|
|
163
|
-
|
|
164
|
-
if docker_compose_version is None:
|
|
149
|
+
# Docker Desktop reports versions like "2.29.7-desktop.1", so only match the prefix
|
|
150
|
+
match = re.search(r"^(\d+\.\d+\.\d+)", version_output)
|
|
151
|
+
if match is None:
|
|
165
152
|
console.warning(
|
|
166
|
-
f"Unable to detect Docker Compose version, attempting to install v{
|
|
153
|
+
f"Unable to detect Docker Compose version, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}"
|
|
167
154
|
)
|
|
168
|
-
elif version.parse(
|
|
169
|
-
MINIMUM_DOCKER_COMPOSE_VERSION
|
|
170
|
-
):
|
|
155
|
+
elif version.parse(match.group(1)) < version.parse(MINIMUM_DOCKER_COMPOSE_VERSION):
|
|
171
156
|
console.warning(
|
|
172
|
-
f"Docker Compose
|
|
157
|
+
f"Docker Compose v{match.group(1)} is older than the minimum supported v{MINIMUM_DOCKER_COMPOSE_VERSION}, attempting to install v{DOCKER_COMPOSE_INSTALL_VERSION}"
|
|
173
158
|
)
|
|
174
|
-
|
|
175
|
-
MINIMUM_DOCKER_COMPOSE_VERSION
|
|
176
|
-
):
|
|
159
|
+
else:
|
|
177
160
|
return
|
|
178
161
|
install_docker_compose()
|
|
179
162
|
|
|
@@ -3,13 +3,23 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import tempfile
|
|
6
|
-
from urllib.request import
|
|
6
|
+
from urllib.request import urlopen
|
|
7
7
|
|
|
8
8
|
from devservices.constants import BINARY_PERMISSIONS
|
|
9
|
+
from devservices.constants import DOWNLOAD_TIMEOUT
|
|
9
10
|
from devservices.exceptions import BinaryInstallError
|
|
10
11
|
from devservices.utils.console import Console
|
|
11
12
|
from devservices.utils.retry import retry
|
|
12
13
|
|
|
14
|
+
DOWNLOAD_ATTEMPTS = 3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def download_file(url: str, path: str) -> None:
|
|
18
|
+
# urlretrieve has no timeout, so a stalled connection would hang forever.
|
|
19
|
+
# The timeout applies to connecting and to each read, not the whole download.
|
|
20
|
+
with urlopen(url, timeout=DOWNLOAD_TIMEOUT) as response, open(path, "wb") as f:
|
|
21
|
+
shutil.copyfileobj(response, f)
|
|
22
|
+
|
|
13
23
|
|
|
14
24
|
def install_binary(
|
|
15
25
|
binary_name: str,
|
|
@@ -24,16 +34,17 @@ def install_binary(
|
|
|
24
34
|
console.info(f"Downloading {binary_name} {version} from {url}...")
|
|
25
35
|
try:
|
|
26
36
|
retry(
|
|
27
|
-
lambda:
|
|
28
|
-
retries=
|
|
37
|
+
lambda: download_file(url, temp_file),
|
|
38
|
+
retries=DOWNLOAD_ATTEMPTS,
|
|
29
39
|
delay=1.0,
|
|
40
|
+
backoff=2.0,
|
|
30
41
|
on_retry=lambda e, remaining: console.warning(
|
|
31
|
-
f"Download failed. Retrying
|
|
42
|
+
f"Download failed ({e}). Retrying... ({remaining} retries left)"
|
|
32
43
|
),
|
|
33
44
|
)
|
|
34
45
|
except Exception as e:
|
|
35
46
|
raise BinaryInstallError(
|
|
36
|
-
f"Failed to download {binary_name} after
|
|
47
|
+
f"Failed to download {binary_name} after {DOWNLOAD_ATTEMPTS} attempts: {e}"
|
|
37
48
|
) from e
|
|
38
49
|
|
|
39
50
|
try:
|
|
@@ -69,10 +69,10 @@ def make_urlopen_response(zip_bytes: bytes) -> mock.MagicMock:
|
|
|
69
69
|
|
|
70
70
|
def url_dispatch(
|
|
71
71
|
zip_bytes_by_repo_name: dict[str, bytes],
|
|
72
|
-
) -> Callable[
|
|
72
|
+
) -> Callable[..., mock.MagicMock]:
|
|
73
73
|
"""Return a urlopen side_effect that routes requests by repo name in the URL."""
|
|
74
74
|
|
|
75
|
-
def _side_effect(request: mock.MagicMock) -> mock.MagicMock:
|
|
75
|
+
def _side_effect(request: mock.MagicMock, timeout: float) -> mock.MagicMock:
|
|
76
76
|
url = request.full_url
|
|
77
77
|
for repo_name, zip_bytes in zip_bytes_by_repo_name.items():
|
|
78
78
|
if f"/{repo_name}/" in url:
|
|
@@ -11,6 +11,7 @@ import pytest
|
|
|
11
11
|
from devservices.configs.service_config import load_service_config_from_file
|
|
12
12
|
from devservices.constants import CONFIG_FILE_NAME
|
|
13
13
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
14
|
+
from devservices.constants import MINIMUM_DOCKER_COMPOSE_VERSION
|
|
14
15
|
from devservices.exceptions import BinaryInstallError
|
|
15
16
|
from devservices.exceptions import DockerComposeError
|
|
16
17
|
from devservices.exceptions import DockerComposeInstallationError
|
|
@@ -44,9 +45,13 @@ def mock_docker_compose_config_services(
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
@mock.patch("subprocess.run")
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
@mock.patch("devservices.utils.docker_compose.install_docker_compose")
|
|
49
|
+
def test_check_docker_compose_version_success(
|
|
50
|
+
mock_install_docker_compose: mock.Mock, mock_run: mock.Mock
|
|
51
|
+
) -> None:
|
|
52
|
+
mock_run.return_value.stdout = f"{MINIMUM_DOCKER_COMPOSE_VERSION}\n"
|
|
49
53
|
check_docker_compose_version() # Should not raise any exception
|
|
54
|
+
mock_install_docker_compose.assert_not_called()
|
|
50
55
|
|
|
51
56
|
|
|
52
57
|
@mock.patch("subprocess.run")
|
|
@@ -78,7 +83,33 @@ def test_check_docker_compose_version_unsupported(
|
|
|
78
83
|
) -> None:
|
|
79
84
|
mock_run.return_value.stdout = "2.20.0-desktop.1\n"
|
|
80
85
|
check_docker_compose_version()
|
|
81
|
-
|
|
86
|
+
mock_install_docker_compose.assert_called_once()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize(
|
|
90
|
+
"installed_version",
|
|
91
|
+
["2.29.7", "2.29.7-desktop.1", "2.38.2", "2.40.3-desktop.1", "5.6.0", "6.0.0"],
|
|
92
|
+
)
|
|
93
|
+
@mock.patch("subprocess.run")
|
|
94
|
+
@mock.patch("devservices.utils.docker_compose.install_docker_compose")
|
|
95
|
+
def test_check_docker_compose_version_at_or_above_minimum(
|
|
96
|
+
mock_install_docker_compose: mock.Mock,
|
|
97
|
+
mock_run: mock.Mock,
|
|
98
|
+
installed_version: str,
|
|
99
|
+
) -> None:
|
|
100
|
+
mock_run.return_value.stdout = f"{installed_version}\n"
|
|
101
|
+
check_docker_compose_version()
|
|
102
|
+
mock_install_docker_compose.assert_not_called()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mock.patch("subprocess.run")
|
|
106
|
+
@mock.patch("devservices.utils.docker_compose.install_docker_compose")
|
|
107
|
+
def test_check_docker_compose_version_below_minimum(
|
|
108
|
+
mock_install_docker_compose: mock.Mock, mock_run: mock.Mock
|
|
109
|
+
) -> None:
|
|
110
|
+
mock_run.return_value.stdout = "2.29.6\n"
|
|
111
|
+
check_docker_compose_version()
|
|
112
|
+
mock_install_docker_compose.assert_called_once()
|
|
82
113
|
|
|
83
114
|
|
|
84
115
|
@mock.patch("subprocess.run")
|
|
@@ -186,7 +217,7 @@ def test_install_docker_compose_binary_install_error(
|
|
|
186
217
|
|
|
187
218
|
@mock.patch("platform.system", return_value="Darwin")
|
|
188
219
|
@mock.patch("platform.machine", return_value="arm64")
|
|
189
|
-
@mock.patch("devservices.utils.install_binary.
|
|
220
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
190
221
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
191
222
|
@mock.patch("devservices.utils.install_binary.shutil.move")
|
|
192
223
|
@mock.patch(
|
|
@@ -202,7 +233,7 @@ def test_install_docker_compose_compose_verification_error(
|
|
|
202
233
|
_mock_get_docker_compose_version: mock.Mock,
|
|
203
234
|
_mock_shutil_move: mock.Mock,
|
|
204
235
|
_mock_chmod: mock.Mock,
|
|
205
|
-
|
|
236
|
+
_mock_download_file: mock.Mock,
|
|
206
237
|
_mock_machine: mock.Mock,
|
|
207
238
|
_mock_system: mock.Mock,
|
|
208
239
|
) -> None:
|
|
@@ -216,26 +247,26 @@ def test_install_docker_compose_compose_verification_error(
|
|
|
216
247
|
@mock.patch("devservices.utils.install_binary.tempfile.TemporaryDirectory")
|
|
217
248
|
@mock.patch("platform.system", return_value="Darwin")
|
|
218
249
|
@mock.patch("platform.machine", return_value="arm64")
|
|
219
|
-
@mock.patch("devservices.utils.install_binary.
|
|
250
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
220
251
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
221
252
|
@mock.patch("devservices.utils.install_binary.shutil.move")
|
|
222
253
|
@mock.patch(
|
|
223
254
|
"devservices.utils.docker_compose.get_docker_compose_version",
|
|
224
|
-
return_value="
|
|
255
|
+
return_value="5.6.0\n",
|
|
225
256
|
)
|
|
226
257
|
def test_install_docker_compose_macos_arm64(
|
|
227
258
|
mock_get_docker_compose_version: mock.Mock,
|
|
228
259
|
mock_shutil_move: mock.Mock,
|
|
229
260
|
mock_chmod: mock.Mock,
|
|
230
|
-
|
|
261
|
+
mock_download_file: mock.Mock,
|
|
231
262
|
_mock_machine: mock.Mock,
|
|
232
263
|
_mock_system: mock.Mock,
|
|
233
264
|
mock_tempdir: mock.Mock,
|
|
234
265
|
) -> None:
|
|
235
266
|
mock_tempdir.return_value.__enter__.return_value = "tempdir"
|
|
236
267
|
install_docker_compose()
|
|
237
|
-
|
|
238
|
-
"https://
|
|
268
|
+
mock_download_file.assert_called_once_with(
|
|
269
|
+
"https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-darwin-aarch64",
|
|
239
270
|
"tempdir/docker-compose",
|
|
240
271
|
)
|
|
241
272
|
mock_chmod.assert_called_once_with("tempdir/docker-compose", 0o755)
|
|
@@ -249,26 +280,26 @@ def test_install_docker_compose_macos_arm64(
|
|
|
249
280
|
@mock.patch("devservices.utils.install_binary.tempfile.TemporaryDirectory")
|
|
250
281
|
@mock.patch("platform.system", return_value="Linux")
|
|
251
282
|
@mock.patch("platform.machine", return_value="x86_64")
|
|
252
|
-
@mock.patch("devservices.utils.install_binary.
|
|
283
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
253
284
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
254
285
|
@mock.patch("devservices.utils.install_binary.shutil.move")
|
|
255
286
|
@mock.patch(
|
|
256
287
|
"devservices.utils.docker_compose.get_docker_compose_version",
|
|
257
|
-
return_value="
|
|
288
|
+
return_value="5.6.0\n",
|
|
258
289
|
)
|
|
259
290
|
def test_install_docker_compose_linux_x86(
|
|
260
291
|
mock_get_docker_compose_version: mock.Mock,
|
|
261
292
|
mock_shutil_move: mock.Mock,
|
|
262
293
|
mock_chmod: mock.Mock,
|
|
263
|
-
|
|
294
|
+
mock_download_file: mock.Mock,
|
|
264
295
|
_mock_machine: mock.Mock,
|
|
265
296
|
_mock_system: mock.Mock,
|
|
266
297
|
mock_tempdir: mock.Mock,
|
|
267
298
|
) -> None:
|
|
268
299
|
mock_tempdir.return_value.__enter__.return_value = "tempdir"
|
|
269
300
|
install_docker_compose()
|
|
270
|
-
|
|
271
|
-
"https://
|
|
301
|
+
mock_download_file.assert_called_once_with(
|
|
302
|
+
"https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-linux-x86_64",
|
|
272
303
|
"tempdir/docker-compose",
|
|
273
304
|
)
|
|
274
305
|
mock_chmod.assert_called_once_with("tempdir/docker-compose", 0o755)
|
|
@@ -282,18 +313,18 @@ def test_install_docker_compose_linux_x86(
|
|
|
282
313
|
@mock.patch("devservices.utils.install_binary.tempfile.TemporaryDirectory")
|
|
283
314
|
@mock.patch("platform.system", return_value="Darwin")
|
|
284
315
|
@mock.patch("platform.machine", return_value="arm64")
|
|
285
|
-
@mock.patch("devservices.utils.install_binary.
|
|
316
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
286
317
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
287
318
|
@mock.patch("devservices.utils.install_binary.shutil.move")
|
|
288
319
|
@mock.patch(
|
|
289
320
|
"devservices.utils.docker_compose.get_docker_compose_version",
|
|
290
|
-
return_value="
|
|
321
|
+
return_value="5.6.0\n",
|
|
291
322
|
)
|
|
292
323
|
def test_install_docker_compose_custom_docker_config_dir(
|
|
293
324
|
mock_get_docker_compose_version: mock.Mock,
|
|
294
325
|
mock_shutil_move: mock.Mock,
|
|
295
326
|
mock_chmod: mock.Mock,
|
|
296
|
-
|
|
327
|
+
mock_download_file: mock.Mock,
|
|
297
328
|
_mock_machine: mock.Mock,
|
|
298
329
|
_mock_system: mock.Mock,
|
|
299
330
|
mock_tempdir: mock.Mock,
|
|
@@ -304,8 +335,8 @@ def test_install_docker_compose_custom_docker_config_dir(
|
|
|
304
335
|
"tempdir/docker/config/cli-plugins",
|
|
305
336
|
):
|
|
306
337
|
install_docker_compose()
|
|
307
|
-
|
|
308
|
-
"https://
|
|
338
|
+
mock_download_file.assert_called_once_with(
|
|
339
|
+
"https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-darwin-aarch64",
|
|
309
340
|
"tempdir/docker-compose",
|
|
310
341
|
)
|
|
311
342
|
mock_chmod.assert_called_once_with("tempdir/docker-compose", 0o755)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import io
|
|
4
|
+
import pathlib
|
|
3
5
|
from unittest import mock
|
|
4
6
|
|
|
5
7
|
import pytest
|
|
6
8
|
|
|
9
|
+
from devservices.constants import DOWNLOAD_TIMEOUT
|
|
7
10
|
from devservices.exceptions import BinaryInstallError
|
|
11
|
+
from devservices.utils.install_binary import download_file
|
|
8
12
|
from devservices.utils.install_binary import install_binary
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
@mock.patch(
|
|
12
|
-
"devservices.utils.install_binary.
|
|
16
|
+
"devservices.utils.install_binary.download_file",
|
|
13
17
|
side_effect=Exception("Connection error"),
|
|
14
18
|
)
|
|
15
19
|
def test_install_docker_compose_connection_error(
|
|
16
|
-
|
|
20
|
+
mock_download_file: mock.Mock, capsys: pytest.CaptureFixture[str]
|
|
17
21
|
) -> None:
|
|
18
22
|
with pytest.raises(
|
|
19
23
|
BinaryInstallError,
|
|
@@ -33,9 +37,9 @@ def test_install_docker_compose_connection_error(
|
|
|
33
37
|
assert "Download failed. Retrying in 1 seconds... (Attempt 2/2)" in captured.out
|
|
34
38
|
|
|
35
39
|
|
|
36
|
-
@mock.patch("devservices.utils.install_binary.
|
|
40
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
37
41
|
def test_install_docker_compose_chmod_file_not_found_error(
|
|
38
|
-
|
|
42
|
+
mock_download_file: mock.Mock,
|
|
39
43
|
) -> None:
|
|
40
44
|
with pytest.raises(
|
|
41
45
|
BinaryInstallError,
|
|
@@ -49,11 +53,11 @@ def test_install_docker_compose_chmod_file_not_found_error(
|
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
|
|
52
|
-
@mock.patch("devservices.utils.install_binary.
|
|
56
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
53
57
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
54
58
|
def test_install_docker_compose_shutil_file_not_found_error(
|
|
55
59
|
mock_chmod: mock.Mock,
|
|
56
|
-
|
|
60
|
+
mock_download_file: mock.Mock,
|
|
57
61
|
) -> None:
|
|
58
62
|
with pytest.raises(
|
|
59
63
|
BinaryInstallError,
|
|
@@ -67,14 +71,14 @@ def test_install_docker_compose_shutil_file_not_found_error(
|
|
|
67
71
|
)
|
|
68
72
|
|
|
69
73
|
|
|
70
|
-
@mock.patch("devservices.utils.install_binary.
|
|
74
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
71
75
|
@mock.patch(
|
|
72
76
|
"devservices.utils.install_binary.os.chmod",
|
|
73
77
|
side_effect=PermissionError("Insufficient Permissions"),
|
|
74
78
|
)
|
|
75
79
|
def test_install_docker_compose_chmod_permission_error(
|
|
76
80
|
mock_chmod: mock.Mock,
|
|
77
|
-
|
|
81
|
+
mock_download_file: mock.Mock,
|
|
78
82
|
) -> None:
|
|
79
83
|
with pytest.raises(
|
|
80
84
|
BinaryInstallError,
|
|
@@ -88,7 +92,7 @@ def test_install_docker_compose_chmod_permission_error(
|
|
|
88
92
|
)
|
|
89
93
|
|
|
90
94
|
|
|
91
|
-
@mock.patch("devservices.utils.install_binary.
|
|
95
|
+
@mock.patch("devservices.utils.install_binary.download_file")
|
|
92
96
|
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
93
97
|
@mock.patch(
|
|
94
98
|
"devservices.utils.install_binary.shutil.move",
|
|
@@ -96,7 +100,7 @@ def test_install_docker_compose_chmod_permission_error(
|
|
|
96
100
|
)
|
|
97
101
|
def test_install_docker_compose_shutil_move_permission_error(
|
|
98
102
|
mock_chmod: mock.Mock,
|
|
99
|
-
|
|
103
|
+
mock_download_file: mock.Mock,
|
|
100
104
|
mock_move: mock.Mock,
|
|
101
105
|
) -> None:
|
|
102
106
|
with pytest.raises(
|
|
@@ -109,3 +113,45 @@ def test_install_docker_compose_shutil_move_permission_error(
|
|
|
109
113
|
"1.0.0",
|
|
110
114
|
"http:://example.com",
|
|
111
115
|
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@mock.patch("devservices.utils.install_binary.urlopen")
|
|
119
|
+
def test_download_file_uses_timeout(
|
|
120
|
+
mock_urlopen: mock.Mock, tmp_path: pathlib.Path
|
|
121
|
+
) -> None:
|
|
122
|
+
mock_urlopen.return_value.__enter__.return_value = io.BytesIO(b"binary")
|
|
123
|
+
path = tmp_path / "binary-name"
|
|
124
|
+
|
|
125
|
+
download_file("https://example.com/binary", str(path))
|
|
126
|
+
|
|
127
|
+
mock_urlopen.assert_called_once_with(
|
|
128
|
+
"https://example.com/binary", timeout=DOWNLOAD_TIMEOUT
|
|
129
|
+
)
|
|
130
|
+
assert path.read_bytes() == b"binary"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@mock.patch("devservices.utils.retry.time.sleep")
|
|
134
|
+
@mock.patch(
|
|
135
|
+
"devservices.utils.install_binary.download_file",
|
|
136
|
+
side_effect=[TimeoutError("timed out"), None],
|
|
137
|
+
)
|
|
138
|
+
@mock.patch("devservices.utils.install_binary.os.chmod")
|
|
139
|
+
@mock.patch("devservices.utils.install_binary.shutil.move")
|
|
140
|
+
def test_install_binary_retries_stalled_download(
|
|
141
|
+
mock_move: mock.Mock,
|
|
142
|
+
mock_chmod: mock.Mock,
|
|
143
|
+
mock_download_file: mock.Mock,
|
|
144
|
+
mock_sleep: mock.Mock,
|
|
145
|
+
capsys: pytest.CaptureFixture[str],
|
|
146
|
+
) -> None:
|
|
147
|
+
install_binary(
|
|
148
|
+
"binary-name",
|
|
149
|
+
"exec_path",
|
|
150
|
+
"1.0.0",
|
|
151
|
+
"https://example.com/binary",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert mock_download_file.call_count == 2
|
|
155
|
+
mock_sleep.assert_called_once_with(1.0)
|
|
156
|
+
mock_move.assert_called_once()
|
|
157
|
+
assert "Download failed (timed out)" in capsys.readouterr().out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|