devservices 1.0.5__tar.gz → 1.0.6__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.
Files changed (59) hide show
  1. {devservices-1.0.5 → devservices-1.0.6}/PKG-INFO +1 -1
  2. {devservices-1.0.5 → devservices-1.0.6}/README.md +1 -1
  3. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/down.py +8 -3
  4. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/purge.py +10 -13
  5. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/update.py +1 -1
  6. {devservices-1.0.5 → devservices-1.0.6}/devservices/constants.py +11 -0
  7. {devservices-1.0.5 → devservices-1.0.6}/devservices/exceptions.py +8 -2
  8. {devservices-1.0.5 → devservices-1.0.6}/devservices/main.py +2 -2
  9. devservices-1.0.6/devservices/utils/check_for_update.py +59 -0
  10. devservices-1.0.6/devservices/utils/docker.py +88 -0
  11. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/PKG-INFO +1 -1
  12. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/SOURCES.txt +2 -1
  13. {devservices-1.0.5 → devservices-1.0.6}/pyproject.toml +1 -1
  14. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_purge.py +98 -51
  15. devservices-1.0.6/tests/utils/test_check_for_update.py +170 -0
  16. devservices-1.0.6/tests/utils/test_docker.py +182 -0
  17. devservices-1.0.5/devservices/commands/check_for_update.py +0 -14
  18. devservices-1.0.5/devservices/utils/docker.py +0 -36
  19. devservices-1.0.5/tests/utils/test_docker.py +0 -47
  20. {devservices-1.0.5 → devservices-1.0.6}/LICENSE.md +0 -0
  21. {devservices-1.0.5 → devservices-1.0.6}/devservices/__init__.py +0 -0
  22. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/__init__.py +0 -0
  23. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/list_dependencies.py +0 -0
  24. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/list_services.py +0 -0
  25. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/logs.py +0 -0
  26. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/status.py +0 -0
  27. {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/up.py +0 -0
  28. {devservices-1.0.5 → devservices-1.0.6}/devservices/configs/service_config.py +0 -0
  29. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/__init__.py +0 -0
  30. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/console.py +0 -0
  31. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/dependencies.py +0 -0
  32. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/devenv.py +0 -0
  33. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/docker_compose.py +0 -0
  34. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/file_lock.py +0 -0
  35. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/install_binary.py +0 -0
  36. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/services.py +0 -0
  37. {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/state.py +0 -0
  38. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/dependency_links.txt +0 -0
  39. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/entry_points.txt +0 -0
  40. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/requires.txt +0 -0
  41. {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/top_level.txt +0 -0
  42. {devservices-1.0.5 → devservices-1.0.6}/setup.cfg +0 -0
  43. {devservices-1.0.5 → devservices-1.0.6}/testing/__init__.py +0 -0
  44. {devservices-1.0.5 → devservices-1.0.6}/testing/utils.py +0 -0
  45. {devservices-1.0.5 → devservices-1.0.6}/tests/__init__.py +0 -0
  46. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_down.py +0 -0
  47. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_list_dependencies.py +0 -0
  48. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_list_services.py +0 -0
  49. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_logs.py +0 -0
  50. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_status.py +0 -0
  51. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_up.py +0 -0
  52. {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_update.py +0 -0
  53. {devservices-1.0.5 → devservices-1.0.6}/tests/configs/test_service_config.py +0 -0
  54. {devservices-1.0.5 → devservices-1.0.6}/tests/conftest.py +0 -0
  55. {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_dependencies.py +0 -0
  56. {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_docker_compose.py +0 -0
  57. {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_install_binary.py +0 -0
  58. {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_services.py +0 -0
  59. {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -11,7 +11,7 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
11
11
  The recommended way to install devservices is through a virtualenv in the requirements.txt.
12
12
 
13
13
  ```
14
- devservices==1.0.5
14
+ devservices==1.0.6
15
15
  ```
16
16
 
17
17
 
@@ -90,9 +90,14 @@ def down(args: Namespace) -> None:
90
90
  capture_exception(de)
91
91
  status.failure(str(de))
92
92
  exit(1)
93
- remote_dependencies = get_non_shared_remote_dependencies(
94
- service, remote_dependencies
95
- )
93
+ try:
94
+ remote_dependencies = get_non_shared_remote_dependencies(
95
+ service, remote_dependencies
96
+ )
97
+ except DependencyError as de:
98
+ capture_exception(de)
99
+ status.failure(str(de))
100
+ exit(1)
96
101
  try:
97
102
  _down(service, remote_dependencies, list(mode_dependencies), status)
98
103
  except DockerComposeError as dce:
@@ -8,11 +8,13 @@ from argparse import ArgumentParser
8
8
  from argparse import Namespace
9
9
 
10
10
  from devservices.constants import DEVSERVICES_CACHE_DIR
11
+ from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11
12
  from devservices.constants import DOCKER_NETWORK_NAME
12
13
  from devservices.exceptions import DockerDaemonNotRunningError
14
+ from devservices.exceptions import DockerError
13
15
  from devservices.utils.console import Console
14
16
  from devservices.utils.console import Status
15
- from devservices.utils.docker import stop_all_running_containers
17
+ from devservices.utils.docker import stop_matching_containers
16
18
  from devservices.utils.state import State
17
19
 
18
20
 
@@ -25,14 +27,6 @@ def purge(_args: Namespace) -> None:
25
27
  """Purge the local devservices cache."""
26
28
  console = Console()
27
29
 
28
- # Prompt the user to stop all running containers
29
- should_stop_containers = console.confirm(
30
- "Warning: Purging stops all running containers and clears devservices state. Would you like to continue?"
31
- )
32
- if not should_stop_containers:
33
- console.warning("Purge canceled")
34
- return
35
-
36
30
  if os.path.exists(DEVSERVICES_CACHE_DIR):
37
31
  try:
38
32
  shutil.rmtree(DEVSERVICES_CACHE_DIR)
@@ -42,13 +36,16 @@ def purge(_args: Namespace) -> None:
42
36
  state = State()
43
37
  state.clear_state()
44
38
  with Status(
45
- lambda: console.warning("Stopping all running containers"),
46
- lambda: console.success("All running containers have been stopped"),
39
+ lambda: console.warning("Stopping all running devservices containers"),
40
+ lambda: console.success("All running devservices containers have been stopped"),
47
41
  ):
48
42
  try:
49
- stop_all_running_containers()
43
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
50
44
  except DockerDaemonNotRunningError:
51
- console.warning("The docker daemon not running, no containers to stop")
45
+ console.warning("The docker daemon is not running, no containers to stop")
46
+ except DockerError as e:
47
+ console.failure(f"Failed to stop running devservices containers {e.stderr}")
48
+ exit(1)
52
49
 
53
50
  console.warning("Removing any devservices networks")
54
51
  devservices_networks = (
@@ -7,10 +7,10 @@ from argparse import ArgumentParser
7
7
  from argparse import Namespace
8
8
  from importlib import metadata
9
9
 
10
- from devservices.commands.check_for_update import check_for_update
11
10
  from devservices.constants import DEVSERVICES_DOWNLOAD_URL
12
11
  from devservices.exceptions import BinaryInstallError
13
12
  from devservices.exceptions import DevservicesUpdateError
13
+ from devservices.utils.check_for_update import check_for_update
14
14
  from devservices.utils.console import Console
15
15
  from devservices.utils.install_binary import install_binary
16
16
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from datetime import timedelta
4
5
 
5
6
  MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
6
7
  DEVSERVICES_DIR_NAME = "devservices"
@@ -12,6 +13,7 @@ DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
12
13
  DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
13
14
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
14
15
  STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
16
+ DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
15
17
  DOCKER_COMPOSE_COMMAND_LENGTH = 7
16
18
 
17
19
  DEPENDENCY_CONFIG_VERSION = "v1"
@@ -21,9 +23,18 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
21
23
  "core.sparseCheckout": "true",
22
24
  }
23
25
 
26
+ DEVSERVICES_RELEASES_URL = (
27
+ "https://api.github.com/repos/getsentry/devservices/releases/latest"
28
+ )
24
29
  DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
25
30
  DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
26
31
  BINARY_PERMISSIONS = 0o755
27
32
  MAX_LOG_LINES = "100"
28
33
  LOGGER_NAME = "devservices"
29
34
  DOCKER_NETWORK_NAME = "devservices"
35
+
36
+ # Latest Version Cache
37
+ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
38
+ DEVSERVICES_CACHE_DIR, "latest_version.txt"
39
+ )
40
+ DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
@@ -57,8 +57,8 @@ class DockerComposeInstallationError(BinaryInstallError):
57
57
  pass
58
58
 
59
59
 
60
- class DockerComposeError(Exception):
61
- """Base class for Docker Compose related errors."""
60
+ class DockerError(Exception):
61
+ """Base class for Docker related errors."""
62
62
 
63
63
  def __init__(self, command: str, returncode: int, stdout: str, stderr: str):
64
64
  self.command = command
@@ -67,6 +67,12 @@ class DockerComposeError(Exception):
67
67
  self.stderr = stderr
68
68
 
69
69
 
70
+ class DockerComposeError(DockerError):
71
+ """Base class for Docker Compose related errors."""
72
+
73
+ pass
74
+
75
+
70
76
  class ModeDoesNotExistError(Exception):
71
77
  """Raised when a mode does not exist."""
72
78
 
@@ -23,10 +23,10 @@ from devservices.commands import purge
23
23
  from devservices.commands import status
24
24
  from devservices.commands import up
25
25
  from devservices.commands import update
26
- from devservices.commands.check_for_update import check_for_update
27
26
  from devservices.constants import LOGGER_NAME
28
27
  from devservices.exceptions import DockerComposeInstallationError
29
28
  from devservices.exceptions import DockerDaemonNotRunningError
29
+ from devservices.utils.check_for_update import check_for_update
30
30
  from devservices.utils.console import Console
31
31
  from devservices.utils.docker_compose import check_docker_compose_version
32
32
 
@@ -102,7 +102,7 @@ def main() -> None:
102
102
  else:
103
103
  parser.print_help()
104
104
 
105
- if args.command != "update":
105
+ if args.command != "update" and os.environ.get("CI") != "true":
106
106
  newest_version = check_for_update()
107
107
  if newest_version != current_version:
108
108
  console.warning(
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+ from datetime import timedelta
7
+ from urllib.request import urlopen
8
+
9
+ from devservices.constants import DEVSERVICES_CACHE_DIR
10
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_FILE
11
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
12
+ from devservices.constants import DEVSERVICES_RELEASES_URL
13
+
14
+
15
+ def _delete_cached_version() -> None:
16
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
17
+ os.remove(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
18
+
19
+
20
+ def _get_cache_age() -> timedelta:
21
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
22
+ file_modification_time = datetime.fromtimestamp(
23
+ os.path.getmtime(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
24
+ )
25
+ return datetime.now() - file_modification_time
26
+ return timedelta.max
27
+
28
+
29
+ def _get_cached_version() -> str | None:
30
+ cache_age = _get_cache_age()
31
+ if cache_age < DEVSERVICES_LATEST_VERSION_CACHE_TTL:
32
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "r", encoding="utf-8") as f:
33
+ return f.read()
34
+ else:
35
+ _delete_cached_version()
36
+ return None
37
+
38
+
39
+ def _set_cached_version(latest_version: str) -> None:
40
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "w", encoding="utf-8") as f:
41
+ f.write(latest_version)
42
+
43
+
44
+ def check_for_update() -> str | None:
45
+ os.makedirs(DEVSERVICES_CACHE_DIR, exist_ok=True)
46
+
47
+ cached_version = _get_cached_version()
48
+ if cached_version is not None:
49
+ return cached_version
50
+
51
+ with urlopen(DEVSERVICES_RELEASES_URL) as response:
52
+ if response.status == 200:
53
+ data = json.loads(response.read())
54
+ latest_version = str(data["tag_name"])
55
+
56
+ _set_cached_version(latest_version)
57
+
58
+ return latest_version
59
+ return None
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+ from devservices.exceptions import DockerDaemonNotRunningError
6
+ from devservices.exceptions import DockerError
7
+
8
+
9
+ def check_docker_daemon_running() -> None:
10
+ """Checks if the Docker daemon is running. Raises DockerDaemonNotRunningError if not."""
11
+ try:
12
+ subprocess.run(
13
+ ["docker", "info"],
14
+ capture_output=True,
15
+ text=True,
16
+ check=True,
17
+ )
18
+ except subprocess.CalledProcessError as e:
19
+ raise DockerDaemonNotRunningError from e
20
+
21
+
22
+ def get_matching_containers(label: str) -> list[str]:
23
+ """
24
+ Returns a list of container IDs with the given label
25
+ """
26
+ check_docker_daemon_running()
27
+ try:
28
+ return (
29
+ subprocess.check_output(
30
+ [
31
+ "docker",
32
+ "ps",
33
+ "-q",
34
+ "--filter",
35
+ f"label={label}",
36
+ ],
37
+ stderr=subprocess.DEVNULL,
38
+ )
39
+ .decode()
40
+ .strip()
41
+ .splitlines()
42
+ )
43
+ except subprocess.CalledProcessError as e:
44
+ raise DockerError(
45
+ command=f"docker ps -q --filter label={label}",
46
+ returncode=e.returncode,
47
+ stdout=e.stdout,
48
+ stderr=e.stderr,
49
+ ) from e
50
+
51
+
52
+ def stop_matching_containers(label: str, should_remove: bool = False) -> None:
53
+ """
54
+ Stops all containers with the given label.
55
+ If should_remove is True, the containers will be removed.
56
+ """
57
+ matching_containers = get_matching_containers(label)
58
+ if len(matching_containers) == 0:
59
+ return
60
+ try:
61
+ subprocess.run(
62
+ ["docker", "stop"] + matching_containers,
63
+ check=True,
64
+ stdout=subprocess.DEVNULL,
65
+ stderr=subprocess.DEVNULL,
66
+ )
67
+ except subprocess.CalledProcessError as e:
68
+ raise DockerError(
69
+ command=f"docker stop {' '.join(matching_containers)}",
70
+ returncode=e.returncode,
71
+ stdout=e.stdout,
72
+ stderr=e.stderr,
73
+ ) from e
74
+ if should_remove:
75
+ try:
76
+ subprocess.run(
77
+ ["docker", "rm"] + matching_containers,
78
+ check=True,
79
+ stdout=subprocess.DEVNULL,
80
+ stderr=subprocess.DEVNULL,
81
+ )
82
+ except subprocess.CalledProcessError as e:
83
+ raise DockerError(
84
+ command=f"docker rm {' '.join(matching_containers)}",
85
+ returncode=e.returncode,
86
+ stdout=e.stdout,
87
+ stderr=e.stderr,
88
+ ) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -13,7 +13,6 @@ devservices.egg-info/entry_points.txt
13
13
  devservices.egg-info/requires.txt
14
14
  devservices.egg-info/top_level.txt
15
15
  devservices/commands/__init__.py
16
- devservices/commands/check_for_update.py
17
16
  devservices/commands/down.py
18
17
  devservices/commands/list_dependencies.py
19
18
  devservices/commands/list_services.py
@@ -24,6 +23,7 @@ devservices/commands/up.py
24
23
  devservices/commands/update.py
25
24
  devservices/configs/service_config.py
26
25
  devservices/utils/__init__.py
26
+ devservices/utils/check_for_update.py
27
27
  devservices/utils/console.py
28
28
  devservices/utils/dependencies.py
29
29
  devservices/utils/devenv.py
@@ -46,6 +46,7 @@ tests/commands/test_status.py
46
46
  tests/commands/test_up.py
47
47
  tests/commands/test_update.py
48
48
  tests/configs/test_service_config.py
49
+ tests/utils/test_check_for_update.py
49
50
  tests/utils/test_dependencies.py
50
51
  tests/utils/test_docker.py
51
52
  tests/utils/test_docker_compose.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.0.5"
7
+ version = "1.0.6"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,47 +1,82 @@
1
1
  from __future__ import annotations
2
2
 
3
- import builtins
4
3
  from argparse import Namespace
5
4
  from pathlib import Path
6
5
  from unittest import mock
7
6
 
7
+ import pytest
8
+
8
9
  from devservices.commands.purge import purge
10
+ from devservices.exceptions import DockerDaemonNotRunningError
11
+ from devservices.exceptions import DockerError
9
12
  from devservices.utils.state import State
10
13
 
11
14
 
12
- @mock.patch("devservices.commands.purge.stop_all_running_containers")
13
- def test_purge_not_confirmed(
14
- mock_stop_all_running_containers: mock.Mock, tmp_path: Path
15
+ @mock.patch("devservices.commands.purge.stop_matching_containers")
16
+ @mock.patch("devservices.commands.purge.subprocess.run")
17
+ def test_purge_docker_daemon_not_running(
18
+ mock_run: mock.Mock,
19
+ mock_stop_matching_containers: mock.Mock,
20
+ capsys: pytest.CaptureFixture[str],
21
+ tmp_path: Path,
15
22
  ) -> None:
23
+ mock_stop_matching_containers.side_effect = DockerDaemonNotRunningError()
16
24
  with (
17
25
  mock.patch(
18
26
  "devservices.commands.purge.DEVSERVICES_CACHE_DIR",
19
27
  str(tmp_path / ".devservices-cache"),
20
28
  ),
21
29
  mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
22
- mock.patch.object(builtins, "input", lambda _: "no"),
30
+ mock.patch(
31
+ "devservices.commands.purge.subprocess.check_output",
32
+ return_value=b"",
33
+ ),
23
34
  ):
35
+ # Create a cache file to test purging
36
+ cache_dir = tmp_path / ".devservices-cache"
37
+ cache_dir.mkdir(parents=True, exist_ok=True)
38
+ cache_file = tmp_path / ".devservices-cache" / "test.txt"
39
+ cache_file.write_text("This is a test cache file.")
40
+
41
+ state = State()
42
+ state.update_started_service("test-service", "test-mode")
43
+
44
+ assert cache_file.exists()
45
+ assert state.get_started_services() == ["test-service"]
46
+
24
47
  args = Namespace()
25
48
  purge(args)
26
49
 
27
- mock_stop_all_running_containers.assert_not_called()
50
+ assert not cache_file.exists()
51
+ assert state.get_started_services() == []
28
52
 
53
+ mock_stop_matching_containers.assert_called_once()
54
+ mock_run.assert_not_called()
55
+
56
+ captured = capsys.readouterr()
57
+ assert (
58
+ "The docker daemon is not running, no containers to stop"
59
+ in captured.out.strip()
60
+ )
29
61
 
30
- @mock.patch("devservices.commands.purge.stop_all_running_containers")
62
+
63
+ @mock.patch("devservices.commands.purge.stop_matching_containers")
31
64
  @mock.patch("devservices.commands.purge.subprocess.run")
32
- def test_purge_with_cache_and_state_and_no_running_containers_confirmed(
33
- mock_run: mock.Mock, mock_stop_all_running_containers: mock.Mock, tmp_path: Path
65
+ def test_purge_docker_daemon_docker_error(
66
+ mock_run: mock.Mock,
67
+ mock_stop_matching_containers: mock.Mock,
68
+ capsys: pytest.CaptureFixture[str],
69
+ tmp_path: Path,
34
70
  ) -> None:
71
+ mock_stop_matching_containers.side_effect = DockerError(
72
+ "command", 1, "output", "stderr"
73
+ )
35
74
  with (
36
75
  mock.patch(
37
76
  "devservices.commands.purge.DEVSERVICES_CACHE_DIR",
38
77
  str(tmp_path / ".devservices-cache"),
39
78
  ),
40
79
  mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
41
- mock.patch.object(builtins, "input", lambda _: "yes"),
42
- mock.patch(
43
- "devservices.utils.docker.check_docker_daemon_running", return_value=None
44
- ),
45
80
  mock.patch(
46
81
  "devservices.commands.purge.subprocess.check_output",
47
82
  return_value=b"",
@@ -60,19 +95,26 @@ def test_purge_with_cache_and_state_and_no_running_containers_confirmed(
60
95
  assert state.get_started_services() == ["test-service"]
61
96
 
62
97
  args = Namespace()
63
- purge(args)
98
+ with pytest.raises(SystemExit):
99
+ purge(args)
64
100
 
65
101
  assert not cache_file.exists()
66
102
  assert state.get_started_services() == []
67
103
 
68
- mock_stop_all_running_containers.assert_called_once()
104
+ mock_stop_matching_containers.assert_called_once()
69
105
  mock_run.assert_not_called()
70
106
 
107
+ captured = capsys.readouterr()
108
+ assert (
109
+ "Failed to stop running devservices containers stderr"
110
+ in captured.out.strip()
111
+ )
71
112
 
72
- @mock.patch("devservices.commands.purge.stop_all_running_containers")
113
+
114
+ @mock.patch("devservices.commands.purge.stop_matching_containers")
73
115
  @mock.patch("devservices.commands.purge.subprocess.run")
74
- def test_purge_with_cache_and_state_and_running_containers_with_networks_confirmed(
75
- mock_run: mock.Mock, mock_stop_all_running_containers: mock.Mock, tmp_path: Path
116
+ def test_purge_with_cache_and_state_and_no_running_containers(
117
+ mock_run: mock.Mock, mock_stop_matching_containers: mock.Mock, tmp_path: Path
76
118
  ) -> None:
77
119
  with (
78
120
  mock.patch(
@@ -80,13 +122,12 @@ def test_purge_with_cache_and_state_and_running_containers_with_networks_confirm
80
122
  str(tmp_path / ".devservices-cache"),
81
123
  ),
82
124
  mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
83
- mock.patch.object(builtins, "input", lambda _: "yes"),
84
125
  mock.patch(
85
126
  "devservices.utils.docker.check_docker_daemon_running", return_value=None
86
127
  ),
87
128
  mock.patch(
88
129
  "devservices.commands.purge.subprocess.check_output",
89
- return_value=b"abc\ndef\nghe\n",
130
+ return_value=b"",
90
131
  ),
91
132
  ):
92
133
  # Create a cache file to test purging
@@ -107,35 +148,14 @@ def test_purge_with_cache_and_state_and_running_containers_with_networks_confirm
107
148
  assert not cache_file.exists()
108
149
  assert state.get_started_services() == []
109
150
 
110
- mock_run.assert_has_calls(
111
- [
112
- mock.call(
113
- ["docker", "network", "rm", "abc"],
114
- check=True,
115
- stdout=mock.ANY,
116
- stderr=mock.ANY,
117
- ),
118
- mock.call(
119
- ["docker", "network", "rm", "def"],
120
- check=True,
121
- stdout=mock.ANY,
122
- stderr=mock.ANY,
123
- ),
124
- mock.call(
125
- ["docker", "network", "rm", "ghe"],
126
- check=True,
127
- stdout=mock.ANY,
128
- stderr=mock.ANY,
129
- ),
130
- ]
131
- )
132
- mock_stop_all_running_containers.assert_called_once()
151
+ mock_stop_matching_containers.assert_called_once()
152
+ mock_run.assert_not_called()
133
153
 
134
154
 
135
- @mock.patch("devservices.commands.purge.stop_all_running_containers")
155
+ @mock.patch("devservices.commands.purge.stop_matching_containers")
136
156
  @mock.patch("devservices.commands.purge.subprocess.run")
137
- def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
138
- mock_run: mock.Mock, mock_stop_all_running_containers: mock.Mock, tmp_path: Path
157
+ def test_purge_with_cache_and_state_and_running_containers_with_networks(
158
+ mock_run: mock.Mock, mock_stop_matching_containers: mock.Mock, tmp_path: Path
139
159
  ) -> None:
140
160
  with (
141
161
  mock.patch(
@@ -143,10 +163,13 @@ def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
143
163
  str(tmp_path / ".devservices-cache"),
144
164
  ),
145
165
  mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
146
- mock.patch.object(builtins, "input", lambda _: "no"),
147
166
  mock.patch(
148
167
  "devservices.utils.docker.check_docker_daemon_running", return_value=None
149
168
  ),
169
+ mock.patch(
170
+ "devservices.commands.purge.subprocess.check_output",
171
+ return_value=b"abc\ndef\nghe\n",
172
+ ),
150
173
  ):
151
174
  # Create a cache file to test purging
152
175
  cache_dir = tmp_path / ".devservices-cache"
@@ -157,11 +180,35 @@ def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
157
180
  state = State()
158
181
  state.update_started_service("test-service", "test-mode")
159
182
 
183
+ assert cache_file.exists()
184
+ assert state.get_started_services() == ["test-service"]
185
+
160
186
  args = Namespace()
161
187
  purge(args)
162
188
 
163
- assert cache_file.exists()
164
- assert state.get_started_services() == ["test-service"]
189
+ assert not cache_file.exists()
190
+ assert state.get_started_services() == []
165
191
 
166
- mock_run.assert_not_called()
167
- mock_stop_all_running_containers.assert_not_called()
192
+ mock_stop_matching_containers.assert_called_once()
193
+ mock_run.assert_has_calls(
194
+ [
195
+ mock.call(
196
+ ["docker", "network", "rm", "abc"],
197
+ check=True,
198
+ stdout=mock.ANY,
199
+ stderr=mock.ANY,
200
+ ),
201
+ mock.call(
202
+ ["docker", "network", "rm", "def"],
203
+ check=True,
204
+ stdout=mock.ANY,
205
+ stderr=mock.ANY,
206
+ ),
207
+ mock.call(
208
+ ["docker", "network", "rm", "ghe"],
209
+ check=True,
210
+ stdout=mock.ANY,
211
+ stderr=mock.ANY,
212
+ ),
213
+ ]
214
+ )
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from datetime import timedelta
5
+ from pathlib import Path
6
+ from unittest import mock
7
+
8
+ from freezegun import freeze_time
9
+
10
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
11
+ from devservices.constants import DEVSERVICES_RELEASES_URL
12
+ from devservices.utils.check_for_update import check_for_update
13
+
14
+
15
+ @mock.patch("devservices.utils.check_for_update.urlopen")
16
+ def test_check_for_update_not_cached(mock_urlopen: mock.Mock, tmp_path: Path) -> None:
17
+ mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.0"}').return_value
18
+ mock_response.status = 200
19
+ mock_urlopen.side_effect = [mock_response]
20
+ cache_dir = tmp_path / "cache"
21
+ cached_file = cache_dir / "latest_version.txt"
22
+ with (
23
+ freeze_time("2024-05-14 05:43:21"),
24
+ mock.patch(
25
+ "devservices.utils.check_for_update.os.path.getmtime",
26
+ return_value=datetime.now().timestamp(),
27
+ ),
28
+ mock.patch(
29
+ "devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
30
+ str(cache_dir),
31
+ ),
32
+ mock.patch(
33
+ "devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
34
+ str(cached_file),
35
+ ),
36
+ ):
37
+ assert check_for_update() == "1.0.0"
38
+ mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
39
+
40
+ with cached_file.open("r") as f:
41
+ cached_version = f.read()
42
+ assert cached_version == "1.0.0"
43
+
44
+
45
+ @mock.patch("devservices.utils.check_for_update.urlopen")
46
+ def test_check_for_update_no_cache_not_ok(
47
+ mock_urlopen: mock.Mock, tmp_path: Path
48
+ ) -> None:
49
+ mock_response = mock.mock_open().return_value
50
+ mock_response.status = 500
51
+ mock_urlopen.side_effect = [mock_response]
52
+ cache_dir = tmp_path / "cache"
53
+ cached_file = cache_dir / "latest_version.txt"
54
+ with (
55
+ mock.patch(
56
+ "devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
57
+ str(cache_dir),
58
+ ),
59
+ mock.patch(
60
+ "devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
61
+ str(cached_file),
62
+ ),
63
+ ):
64
+ assert check_for_update() is None
65
+ mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
66
+
67
+ assert not cached_file.exists()
68
+
69
+
70
+ @mock.patch("devservices.utils.check_for_update.urlopen")
71
+ def test_check_for_update_cached_fresh(mock_urlopen: mock.Mock, tmp_path: Path) -> None:
72
+ cache_dir = tmp_path / "cache"
73
+ cached_file = cache_dir / "latest_version.txt"
74
+ with (
75
+ freeze_time("2024-05-14 05:43:21"),
76
+ mock.patch(
77
+ "devservices.utils.check_for_update.os.path.getmtime",
78
+ return_value=(
79
+ datetime.now()
80
+ - DEVSERVICES_LATEST_VERSION_CACHE_TTL
81
+ + timedelta(minutes=1)
82
+ ).timestamp(),
83
+ ),
84
+ mock.patch(
85
+ "devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
86
+ str(cache_dir),
87
+ ),
88
+ mock.patch(
89
+ "devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
90
+ str(cached_file),
91
+ ),
92
+ ):
93
+ cache_dir.mkdir()
94
+ cached_file.write_text("1.0.0")
95
+ assert check_for_update() == "1.0.0"
96
+ mock_urlopen.assert_not_called()
97
+
98
+
99
+ @mock.patch("devservices.utils.check_for_update.urlopen")
100
+ def test_check_for_update_cached_stale_without_update(
101
+ mock_urlopen: mock.Mock, tmp_path: Path
102
+ ) -> None:
103
+ mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.0"}').return_value
104
+ mock_response.status = 200
105
+ mock_urlopen.side_effect = [mock_response]
106
+ cache_dir = tmp_path / "cache"
107
+ cached_file = cache_dir / "latest_version.txt"
108
+ with (
109
+ freeze_time("2024-05-14 05:43:21"),
110
+ mock.patch(
111
+ "devservices.utils.check_for_update.os.path.getmtime",
112
+ return_value=(
113
+ datetime.now() - DEVSERVICES_LATEST_VERSION_CACHE_TTL
114
+ ).timestamp(),
115
+ ),
116
+ mock.patch(
117
+ "devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
118
+ str(cache_dir),
119
+ ),
120
+ mock.patch(
121
+ "devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
122
+ str(cached_file),
123
+ ),
124
+ ):
125
+ cache_dir.mkdir()
126
+ cached_file.write_text("1.0.0")
127
+ assert check_for_update() == "1.0.0"
128
+ mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
129
+
130
+ with cached_file.open("r") as f:
131
+ cached_version = f.read()
132
+ assert cached_version == "1.0.0"
133
+
134
+
135
+ @mock.patch("devservices.utils.check_for_update.urlopen")
136
+ def test_check_for_update_cached_stale_with_update(
137
+ mock_urlopen: mock.Mock, tmp_path: Path
138
+ ) -> None:
139
+ mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.1"}').return_value
140
+ mock_response.status = 200
141
+ mock_urlopen.side_effect = [mock_response]
142
+ cache_dir = tmp_path / "cache"
143
+ cached_file = cache_dir / "latest_version.txt"
144
+ with (
145
+ freeze_time("2024-05-14 05:43:21"),
146
+ mock.patch(
147
+ "devservices.utils.check_for_update.os.path.getmtime",
148
+ return_value=(
149
+ datetime.now()
150
+ - DEVSERVICES_LATEST_VERSION_CACHE_TTL
151
+ - timedelta(minutes=1)
152
+ ).timestamp(),
153
+ ),
154
+ mock.patch(
155
+ "devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
156
+ str(cache_dir),
157
+ ),
158
+ mock.patch(
159
+ "devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
160
+ str(cached_file),
161
+ ),
162
+ ):
163
+ cache_dir.mkdir()
164
+ cached_file.write_text("1.0.0")
165
+ assert check_for_update() == "1.0.1"
166
+ mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
167
+
168
+ with cached_file.open("r") as f:
169
+ cached_data = f.read()
170
+ assert cached_data == "1.0.1"
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from unittest import mock
5
+
6
+ import pytest
7
+
8
+ from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
9
+ from devservices.exceptions import DockerDaemonNotRunningError
10
+ from devservices.exceptions import DockerError
11
+ from devservices.utils.docker import check_docker_daemon_running
12
+ from devservices.utils.docker import get_matching_containers
13
+ from devservices.utils.docker import stop_matching_containers
14
+
15
+
16
+ @mock.patch("subprocess.run")
17
+ def test_check_docker_daemon_running_error(mock_run: mock.Mock) -> None:
18
+ mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")
19
+ with pytest.raises(DockerDaemonNotRunningError):
20
+ check_docker_daemon_running()
21
+ mock_run.assert_called_once_with(
22
+ ["docker", "info"],
23
+ capture_output=True,
24
+ text=True,
25
+ check=True,
26
+ )
27
+
28
+
29
+ @mock.patch("subprocess.run")
30
+ def test_check_docker_daemon_running(mock_run: mock.Mock) -> None:
31
+ check_docker_daemon_running()
32
+ mock_run.assert_called_once_with(
33
+ ["docker", "info"],
34
+ capture_output=True,
35
+ text=True,
36
+ check=True,
37
+ )
38
+
39
+
40
+ @mock.patch("subprocess.check_output")
41
+ @mock.patch("devservices.utils.docker.check_docker_daemon_running")
42
+ def test_get_matching_containers(
43
+ mock_check_docker_daemon_running: mock.Mock,
44
+ mock_check_output: mock.Mock,
45
+ ) -> None:
46
+ mock_check_docker_daemon_running.return_value = None
47
+ mock_check_output.return_value = b""
48
+ get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
49
+ mock_check_docker_daemon_running.assert_called_once()
50
+ mock_check_output.assert_called_once_with(
51
+ ["docker", "ps", "-q", "--filter", f"label={DEVSERVICES_ORCHESTRATOR_LABEL}"],
52
+ stderr=subprocess.DEVNULL,
53
+ )
54
+
55
+
56
+ @mock.patch("subprocess.check_output")
57
+ @mock.patch("devservices.utils.docker.check_docker_daemon_running")
58
+ def test_get_matching_containers_docker_daemon_not_running(
59
+ mock_check_docker_daemon_running: mock.Mock,
60
+ mock_check_output: mock.Mock,
61
+ ) -> None:
62
+ mock_check_docker_daemon_running.side_effect = DockerDaemonNotRunningError()
63
+ with pytest.raises(DockerDaemonNotRunningError):
64
+ get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
65
+ mock_check_docker_daemon_running.assert_called_once()
66
+ mock_check_output.assert_not_called()
67
+
68
+
69
+ @mock.patch("subprocess.check_output")
70
+ @mock.patch("devservices.utils.docker.check_docker_daemon_running")
71
+ def test_get_matching_containers_error(
72
+ mock_check_docker_daemon_running: mock.Mock,
73
+ mock_check_output: mock.Mock,
74
+ ) -> None:
75
+ mock_check_docker_daemon_running.return_value = None
76
+ mock_check_output.side_effect = subprocess.CalledProcessError(1, "cmd")
77
+ with pytest.raises(DockerError):
78
+ get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
79
+ mock_check_docker_daemon_running.assert_called_once()
80
+ mock_check_output.assert_called_once_with(
81
+ ["docker", "ps", "-q", "--filter", f"label={DEVSERVICES_ORCHESTRATOR_LABEL}"],
82
+ stderr=subprocess.DEVNULL,
83
+ )
84
+
85
+
86
+ @mock.patch("subprocess.run")
87
+ @mock.patch("devservices.utils.docker.get_matching_containers")
88
+ def test_stop_matching_containers_should_not_remove(
89
+ mock_get_matching_containers: mock.Mock,
90
+ mock_run: mock.Mock,
91
+ ) -> None:
92
+ mock_get_matching_containers.return_value = ["container1", "container2"]
93
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=False)
94
+ mock_run.assert_called_once_with(
95
+ ["docker", "stop", "container1", "container2"],
96
+ check=True,
97
+ stdout=subprocess.DEVNULL,
98
+ stderr=subprocess.DEVNULL,
99
+ )
100
+
101
+
102
+ @mock.patch("subprocess.run")
103
+ @mock.patch("devservices.utils.docker.get_matching_containers")
104
+ def test_stop_matching_containers_none(
105
+ mock_get_matching_containers: mock.Mock,
106
+ mock_run: mock.Mock,
107
+ ) -> None:
108
+ mock_get_matching_containers.return_value = []
109
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
110
+ mock_run.assert_not_called()
111
+
112
+
113
+ @mock.patch("subprocess.run")
114
+ @mock.patch("devservices.utils.docker.get_matching_containers")
115
+ def test_stop_matching_containers_should_remove(
116
+ mock_get_matching_containers: mock.Mock,
117
+ mock_run: mock.Mock,
118
+ ) -> None:
119
+ mock_get_matching_containers.return_value = ["container1", "container2"]
120
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
121
+ mock_run.assert_has_calls(
122
+ [
123
+ mock.call(
124
+ ["docker", "stop", "container1", "container2"],
125
+ check=True,
126
+ stdout=subprocess.DEVNULL,
127
+ stderr=subprocess.DEVNULL,
128
+ ),
129
+ mock.call(
130
+ ["docker", "rm", "container1", "container2"],
131
+ check=True,
132
+ stdout=subprocess.DEVNULL,
133
+ stderr=subprocess.DEVNULL,
134
+ ),
135
+ ]
136
+ )
137
+
138
+
139
+ @mock.patch("subprocess.run")
140
+ @mock.patch("devservices.utils.docker.get_matching_containers")
141
+ def test_stop_matching_containers_stop_error(
142
+ mock_get_matching_containers: mock.Mock,
143
+ mock_run: mock.Mock,
144
+ ) -> None:
145
+ mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")
146
+ mock_get_matching_containers.return_value = ["container1", "container2"]
147
+ with pytest.raises(DockerError):
148
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
149
+ mock_run.assert_called_once_with(
150
+ ["docker", "stop", "container1", "container2"],
151
+ check=True,
152
+ stdout=subprocess.DEVNULL,
153
+ stderr=subprocess.DEVNULL,
154
+ )
155
+
156
+
157
+ @mock.patch("subprocess.run")
158
+ @mock.patch("devservices.utils.docker.get_matching_containers")
159
+ def test_stop_matching_containers_remove_error(
160
+ mock_get_matching_containers: mock.Mock,
161
+ mock_run: mock.Mock,
162
+ ) -> None:
163
+ mock_run.side_effect = [None, subprocess.CalledProcessError(1, "cmd")]
164
+ mock_get_matching_containers.return_value = ["container1", "container2"]
165
+ with pytest.raises(DockerError):
166
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
167
+ mock_run.assert_has_calls(
168
+ [
169
+ mock.call(
170
+ ["docker", "stop", "container1", "container2"],
171
+ check=True,
172
+ stdout=subprocess.DEVNULL,
173
+ stderr=subprocess.DEVNULL,
174
+ ),
175
+ mock.call(
176
+ ["docker", "rm", "container1", "container2"],
177
+ check=True,
178
+ stdout=subprocess.DEVNULL,
179
+ stderr=subprocess.DEVNULL,
180
+ ),
181
+ ]
182
+ )
@@ -1,14 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from urllib.request import urlopen
5
-
6
-
7
- def check_for_update() -> str | None:
8
- url = "https://api.github.com/repos/getsentry/devservices/releases/latest"
9
- with urlopen(url) as response:
10
- if response.status == 200:
11
- data = json.loads(response.read().decode("utf-8"))
12
- latest_version = str(data["tag_name"])
13
- return latest_version
14
- return None
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import subprocess
4
-
5
- from devservices.exceptions import DockerDaemonNotRunningError
6
-
7
-
8
- def check_docker_daemon_running() -> None:
9
- """Checks if the Docker daemon is running. Raises DockerDaemonNotRunningError if not."""
10
- try:
11
- subprocess.run(
12
- ["docker", "info"],
13
- capture_output=True,
14
- text=True,
15
- check=True,
16
- )
17
- except subprocess.CalledProcessError as e:
18
- raise DockerDaemonNotRunningError from e
19
-
20
-
21
- def stop_all_running_containers() -> None:
22
- check_docker_daemon_running()
23
- running_containers = (
24
- subprocess.check_output(["docker", "ps", "-q"], stderr=subprocess.DEVNULL)
25
- .decode()
26
- .strip()
27
- .splitlines()
28
- )
29
- if len(running_containers) == 0:
30
- return
31
- subprocess.run(
32
- ["docker", "stop"] + running_containers,
33
- check=True,
34
- stdout=subprocess.DEVNULL,
35
- stderr=subprocess.DEVNULL,
36
- )
@@ -1,47 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import subprocess
4
- from unittest import mock
5
-
6
- from devservices.utils.docker import stop_all_running_containers
7
-
8
-
9
- @mock.patch("subprocess.check_output")
10
- @mock.patch("subprocess.run")
11
- @mock.patch("devservices.utils.docker.check_docker_daemon_running")
12
- def test_stop_all_running_containers_none_running(
13
- mock_check_docker_daemon_running: mock.Mock,
14
- mock_run: mock.Mock,
15
- mock_check_output: mock.Mock,
16
- ) -> None:
17
- mock_check_docker_daemon_running.return_value = None
18
- mock_check_output.return_value = b""
19
- stop_all_running_containers()
20
- mock_check_docker_daemon_running.assert_called_once()
21
- mock_check_output.assert_called_once_with(
22
- ["docker", "ps", "-q"], stderr=subprocess.DEVNULL
23
- )
24
- mock_run.assert_not_called()
25
-
26
-
27
- @mock.patch("subprocess.check_output")
28
- @mock.patch("subprocess.run")
29
- @mock.patch("devservices.utils.docker.check_docker_daemon_running")
30
- def test_stop_all_running_containers(
31
- mock_check_docker_daemon_running: mock.Mock,
32
- mock_run: mock.Mock,
33
- mock_check_output: mock.Mock,
34
- ) -> None:
35
- mock_check_docker_daemon_running.return_value = None
36
- mock_check_output.return_value = b"container1\ncontainer2\n"
37
- stop_all_running_containers()
38
- mock_check_docker_daemon_running.assert_called_once()
39
- mock_check_output.assert_called_once_with(
40
- ["docker", "ps", "-q"], stderr=subprocess.DEVNULL
41
- )
42
- mock_run.assert_called_once_with(
43
- ["docker", "stop", "container1", "container2"],
44
- check=True,
45
- stdout=subprocess.DEVNULL,
46
- stderr=subprocess.DEVNULL,
47
- )
File without changes
File without changes