devservices 1.0.4__tar.gz → 1.0.5__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 (55) hide show
  1. {devservices-1.0.4 → devservices-1.0.5}/PKG-INFO +1 -1
  2. {devservices-1.0.4 → devservices-1.0.5}/README.md +1 -1
  3. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/down.py +13 -5
  4. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/list_dependencies.py +5 -2
  5. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/list_services.py +2 -0
  6. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/logs.py +4 -1
  7. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/status.py +4 -1
  8. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/up.py +9 -57
  9. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/dependencies.py +37 -10
  10. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/devenv.py +1 -1
  11. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/state.py +20 -11
  12. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/PKG-INFO +1 -1
  13. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/SOURCES.txt +3 -0
  14. {devservices-1.0.4 → devservices-1.0.5}/pyproject.toml +1 -1
  15. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_down.py +35 -3
  16. devservices-1.0.5/tests/commands/test_list_dependencies.py +111 -0
  17. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_list_services.py +4 -4
  18. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_logs.py +33 -0
  19. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_purge.py +3 -3
  20. devservices-1.0.5/tests/commands/test_status.py +171 -0
  21. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_up.py +53 -58
  22. {devservices-1.0.4 → devservices-1.0.5}/tests/utils/test_dependencies.py +162 -15
  23. devservices-1.0.5/tests/utils/test_services.py +102 -0
  24. {devservices-1.0.4 → devservices-1.0.5}/tests/utils/test_state.py +10 -10
  25. {devservices-1.0.4 → devservices-1.0.5}/LICENSE.md +0 -0
  26. {devservices-1.0.4 → devservices-1.0.5}/devservices/__init__.py +0 -0
  27. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/__init__.py +0 -0
  28. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/check_for_update.py +0 -0
  29. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/purge.py +0 -0
  30. {devservices-1.0.4 → devservices-1.0.5}/devservices/commands/update.py +0 -0
  31. {devservices-1.0.4 → devservices-1.0.5}/devservices/configs/service_config.py +0 -0
  32. {devservices-1.0.4 → devservices-1.0.5}/devservices/constants.py +0 -0
  33. {devservices-1.0.4 → devservices-1.0.5}/devservices/exceptions.py +0 -0
  34. {devservices-1.0.4 → devservices-1.0.5}/devservices/main.py +0 -0
  35. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/__init__.py +0 -0
  36. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/console.py +0 -0
  37. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/docker.py +0 -0
  38. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/docker_compose.py +0 -0
  39. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/file_lock.py +0 -0
  40. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/install_binary.py +0 -0
  41. {devservices-1.0.4 → devservices-1.0.5}/devservices/utils/services.py +0 -0
  42. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/dependency_links.txt +0 -0
  43. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/entry_points.txt +0 -0
  44. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/requires.txt +0 -0
  45. {devservices-1.0.4 → devservices-1.0.5}/devservices.egg-info/top_level.txt +0 -0
  46. {devservices-1.0.4 → devservices-1.0.5}/setup.cfg +0 -0
  47. {devservices-1.0.4 → devservices-1.0.5}/testing/__init__.py +0 -0
  48. {devservices-1.0.4 → devservices-1.0.5}/testing/utils.py +0 -0
  49. {devservices-1.0.4 → devservices-1.0.5}/tests/__init__.py +0 -0
  50. {devservices-1.0.4 → devservices-1.0.5}/tests/commands/test_update.py +0 -0
  51. {devservices-1.0.4 → devservices-1.0.5}/tests/configs/test_service_config.py +0 -0
  52. {devservices-1.0.4 → devservices-1.0.5}/tests/conftest.py +0 -0
  53. {devservices-1.0.4 → devservices-1.0.5}/tests/utils/test_docker.py +0 -0
  54. {devservices-1.0.4 → devservices-1.0.5}/tests/utils/test_docker_compose.py +0 -0
  55. {devservices-1.0.4 → devservices-1.0.5}/tests/utils/test_install_binary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.4
3
+ Version: 1.0.5
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.4
14
+ devservices==1.0.5
15
15
  ```
16
16
 
17
17
 
@@ -56,10 +56,13 @@ def down(args: Namespace) -> None:
56
56
  service_name = args.service_name
57
57
  try:
58
58
  service = find_matching_service(service_name)
59
- except (ConfigError, ServiceNotFoundError) as e:
59
+ except ConfigError as e:
60
60
  capture_exception(e)
61
61
  console.failure(str(e))
62
62
  exit(1)
63
+ except ServiceNotFoundError as e:
64
+ console.failure(str(e))
65
+ exit(1)
63
66
 
64
67
  modes = service.config.modes
65
68
 
@@ -69,15 +72,20 @@ def down(args: Namespace) -> None:
69
72
  console.warning(f"{service.name} is not running")
70
73
  exit(0)
71
74
 
72
- mode = state.get_mode_for_service(service.name) or "default"
73
- mode_dependencies = modes[mode]
75
+ active_modes = state.get_active_modes_for_service(service.name)
76
+ mode_dependencies = set()
77
+ for active_mode in active_modes:
78
+ active_mode_dependencies = modes.get(active_mode, [])
79
+ mode_dependencies.update(active_mode_dependencies)
74
80
 
75
81
  with Status(
76
82
  lambda: console.warning(f"Stopping {service.name}"),
77
83
  lambda: console.success(f"{service.name} stopped"),
78
84
  ) as status:
79
85
  try:
80
- remote_dependencies = install_and_verify_dependencies(service, mode=mode)
86
+ remote_dependencies = install_and_verify_dependencies(
87
+ service, modes=active_modes
88
+ )
81
89
  except DependencyError as de:
82
90
  capture_exception(de)
83
91
  status.failure(str(de))
@@ -86,7 +94,7 @@ def down(args: Namespace) -> None:
86
94
  service, remote_dependencies
87
95
  )
88
96
  try:
89
- _down(service, remote_dependencies, mode_dependencies, status)
97
+ _down(service, remote_dependencies, list(mode_dependencies), status)
90
98
  except DockerComposeError as dce:
91
99
  capture_exception(dce)
92
100
  status.failure(f"Failed to stop {service.name}: {dce.stderr}")
@@ -32,10 +32,13 @@ def list_dependencies(args: Namespace) -> None:
32
32
 
33
33
  try:
34
34
  service = find_matching_service(service_name)
35
- except (ConfigError, ServiceNotFoundError) as e:
35
+ except ConfigError as e:
36
36
  capture_exception(e)
37
37
  console.failure(str(e))
38
38
  exit(1)
39
+ except ServiceNotFoundError as e:
40
+ console.failure(str(e))
41
+ exit(1)
39
42
 
40
43
  dependencies = service.config.dependencies
41
44
 
@@ -45,4 +48,4 @@ def list_dependencies(args: Namespace) -> None:
45
48
 
46
49
  console.info(f"Dependencies of {service.name}:")
47
50
  for dependency_key, dependency_info in dependencies.items():
48
- console.info("-" + dependency_key + ":" + dependency_info.description)
51
+ console.info("- " + dependency_key + ": " + dependency_info.description)
@@ -47,7 +47,9 @@ def list_services(args: Namespace) -> None:
47
47
 
48
48
  for service in services_to_show:
49
49
  status = "running" if service.name in running_services else "stopped"
50
+ active_modes = state.get_active_modes_for_service(service.name)
50
51
  console.info(f"- {service.name}")
52
+ console.info(f" modes: {active_modes}")
51
53
  console.info(f" status: {status}")
52
54
  console.info(f" location: {service.repo_path}")
53
55
 
@@ -46,10 +46,13 @@ def logs(args: Namespace) -> None:
46
46
  service_name = args.service_name
47
47
  try:
48
48
  service = find_matching_service(service_name)
49
- except (ConfigError, ServiceNotFoundError) as e:
49
+ except ConfigError as e:
50
50
  capture_exception(e)
51
51
  console.failure(str(e))
52
52
  exit(1)
53
+ except ServiceNotFoundError as e:
54
+ console.failure(str(e))
55
+ exit(1)
53
56
 
54
57
  modes = service.config.modes
55
58
  # TODO: allow custom modes to be used
@@ -81,10 +81,13 @@ def status(args: Namespace) -> None:
81
81
  service_name = args.service_name
82
82
  try:
83
83
  service = find_matching_service(service_name)
84
- except (ConfigError, ServiceNotFoundError) as e:
84
+ except ConfigError as e:
85
85
  capture_exception(e)
86
86
  console.failure(str(e))
87
87
  exit(1)
88
+ except ServiceNotFoundError as e:
89
+ console.failure(str(e))
90
+ exit(1)
88
91
 
89
92
  modes = service.config.modes
90
93
  # TODO: allow custom modes to be used
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import concurrent.futures
4
3
  import os
5
4
  import subprocess
6
5
  from argparse import _SubParsersAction
@@ -23,7 +22,6 @@ from devservices.exceptions import ServiceNotFoundError
23
22
  from devservices.utils.console import Console
24
23
  from devservices.utils.console import Status
25
24
  from devservices.utils.dependencies import construct_dependency_graph
26
- from devservices.utils.dependencies import get_non_shared_remote_dependencies
27
25
  from devservices.utils.dependencies import install_and_verify_dependencies
28
26
  from devservices.utils.dependencies import InstalledRemoteDependency
29
27
  from devservices.utils.docker_compose import get_docker_compose_commands_to_run
@@ -58,64 +56,17 @@ def up(args: Namespace) -> None:
58
56
  service_name = args.service_name
59
57
  try:
60
58
  service = find_matching_service(service_name)
61
- except (ConfigError, ServiceNotFoundError) as e:
59
+ except ConfigError as e:
62
60
  capture_exception(e)
63
61
  console.failure(str(e))
64
62
  exit(1)
63
+ except ServiceNotFoundError as e:
64
+ console.failure(str(e))
65
+ exit(1)
65
66
 
66
67
  modes = service.config.modes
67
68
  mode = args.mode
68
69
 
69
- state = State()
70
- started_services = state.get_started_services()
71
- running_mode = state.get_mode_for_service(service.name) or "default"
72
-
73
- # TODO: Remove this once we properly handle mode switching
74
- if service.name in started_services and running_mode != mode:
75
- console.warning(
76
- f"Service '{service.name}' is already running in mode: '{running_mode}', restarting in mode: '{mode}'"
77
- )
78
- with Status() as status:
79
- try:
80
- remote_dependencies = install_and_verify_dependencies(
81
- service, mode=running_mode
82
- )
83
- except DependencyError as de:
84
- capture_exception(de)
85
- status.failure(str(de))
86
- exit(1)
87
- except ModeDoesNotExistError as mde:
88
- capture_exception(mde)
89
- status.failure(str(mde))
90
- exit(1)
91
- service_config_file_path = os.path.join(
92
- service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
93
- )
94
- current_env = os.environ.copy()
95
- running_mode_dependencies = modes[running_mode]
96
- remote_dependencies_to_bring_down = get_non_shared_remote_dependencies(
97
- service, remote_dependencies
98
- )
99
- down_docker_compose_commands = get_docker_compose_commands_to_run(
100
- service=service,
101
- remote_dependencies=list(remote_dependencies_to_bring_down),
102
- current_env=current_env,
103
- command="down",
104
- options=[],
105
- service_config_file_path=service_config_file_path,
106
- mode_dependencies=running_mode_dependencies,
107
- )
108
-
109
- with concurrent.futures.ThreadPoolExecutor() as executor:
110
- futures = [
111
- executor.submit(run_cmd, cmd, current_env)
112
- for cmd in down_docker_compose_commands
113
- ]
114
- for future in concurrent.futures.as_completed(futures):
115
- future.result()
116
-
117
- state.remove_started_service(service.name)
118
-
119
70
  with Status(
120
71
  lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
121
72
  lambda: console.success(f"{service.name} started"),
@@ -123,7 +74,7 @@ def up(args: Namespace) -> None:
123
74
  try:
124
75
  status.info("Retrieving dependencies")
125
76
  remote_dependencies = install_and_verify_dependencies(
126
- service, force_update_dependencies=True, mode=mode
77
+ service, force_update_dependencies=True, modes=[mode]
127
78
  )
128
79
  except DependencyError as de:
129
80
  capture_exception(de)
@@ -139,14 +90,14 @@ def up(args: Namespace) -> None:
139
90
  pass
140
91
  try:
141
92
  mode_dependencies = modes[mode]
142
- _up(service, remote_dependencies, mode_dependencies, status)
93
+ _up(service, [mode], remote_dependencies, mode_dependencies, status)
143
94
  except DockerComposeError as dce:
144
95
  capture_exception(dce)
145
96
  status.failure(f"Failed to start {service.name}: {dce.stderr}")
146
97
  exit(1)
147
98
  # TODO: We should factor in healthchecks here before marking service as running
148
99
  state = State()
149
- state.add_started_service(service.name, mode)
100
+ state.update_started_service(service.name, mode)
150
101
 
151
102
 
152
103
  def _bring_up_dependency(
@@ -160,6 +111,7 @@ def _bring_up_dependency(
160
111
 
161
112
  def _up(
162
113
  service: Service,
114
+ modes: list[str],
163
115
  remote_dependencies: set[InstalledRemoteDependency],
164
116
  mode_dependencies: list[str],
165
117
  status: Status,
@@ -177,7 +129,7 @@ def _up(
177
129
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
178
130
  ] = relative_local_dependency_directory
179
131
  options = ["-d"]
180
- dependency_graph = construct_dependency_graph(service)
132
+ dependency_graph = construct_dependency_graph(service, modes=modes)
181
133
  starting_order = dependency_graph.get_starting_order()
182
134
  sorted_remote_dependencies = sorted(
183
135
  remote_dependencies, key=lambda dep: starting_order.index(dep.service_name)
@@ -160,11 +160,20 @@ class GitConfigManager:
160
160
 
161
161
 
162
162
  def install_and_verify_dependencies(
163
- service: Service, force_update_dependencies: bool = False, mode: str = "default"
163
+ service: Service,
164
+ force_update_dependencies: bool = False,
165
+ modes: list[str] | None = None,
164
166
  ) -> set[InstalledRemoteDependency]:
165
- if mode not in service.config.modes:
166
- raise ModeDoesNotExistError(service_name=service.name, mode=mode)
167
- mode_dependencies = set(service.config.modes[mode])
167
+ """
168
+ Install and verify dependencies for a service
169
+ """
170
+ if modes is None:
171
+ modes = ["default"]
172
+ mode_dependencies = set()
173
+ for mode in modes:
174
+ if mode not in service.config.modes:
175
+ raise ModeDoesNotExistError(service_name=service.name, mode=mode)
176
+ mode_dependencies.update(service.config.modes[mode])
168
177
  matching_dependencies = [
169
178
  dependency
170
179
  for dependency_key, dependency in list(service.config.dependencies.items())
@@ -346,8 +355,18 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
346
355
  branch=dependency.branch,
347
356
  ) from e
348
357
 
349
- nested_dependencies = list(installed_config.dependencies.values())
350
- nested_remote_configs = _get_remote_configs(nested_dependencies)
358
+ if dependency.mode not in installed_config.modes:
359
+ raise ModeDoesNotExistError(
360
+ service_name=installed_config.service_name,
361
+ mode=dependency.mode,
362
+ )
363
+
364
+ active_nested_dependencies = [
365
+ nested_dependency
366
+ for nested_dependency_name, nested_dependency in installed_config.dependencies.items()
367
+ if nested_dependency_name in installed_config.modes[dependency.mode]
368
+ ]
369
+ nested_remote_configs = _get_remote_configs(active_nested_dependencies)
351
370
 
352
371
  installed_dependencies: set[InstalledRemoteDependency] = set(
353
372
  [
@@ -526,15 +545,23 @@ def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
526
545
  return load_service_config_from_file(dependency_repo_dir)
527
546
 
528
547
 
529
- def construct_dependency_graph(service: Service) -> DependencyGraph:
548
+ def construct_dependency_graph(service: Service, modes: list[str]) -> DependencyGraph:
530
549
  dependency_graph = DependencyGraph()
531
550
 
532
- def _construct_dependency_graph(service_config: ServiceConfig) -> None:
551
+ def _construct_dependency_graph(
552
+ service_config: ServiceConfig, modes: list[str]
553
+ ) -> None:
554
+ service_mode_dependencies = set()
555
+ for mode in modes:
556
+ service_mode_dependencies.update(service_config.modes.get(mode, []))
533
557
  for dependency_name, dependency in service_config.dependencies.items():
558
+ # Skip the dependency if it's not in the modes (since it may not be installed and we don't care about it)
559
+ if dependency_name not in service_mode_dependencies:
560
+ continue
534
561
  dependency_graph.add_edge(service_config.service_name, dependency_name)
535
562
  if _has_remote_config(dependency.remote):
536
563
  dependency_config = get_remote_dependency_config(dependency.remote)
537
- _construct_dependency_graph(dependency_config)
564
+ _construct_dependency_graph(dependency_config, [dependency.remote.mode])
538
565
 
539
- _construct_dependency_graph(service.config)
566
+ _construct_dependency_graph(service.config, modes)
540
567
  return dependency_graph
@@ -13,7 +13,7 @@ def get_coderoot() -> str:
13
13
  config_path = os.path.join(home, ".config", "sentry-devenv", "config.ini")
14
14
  try:
15
15
  devenv_config: ConfigParser = read_config(config_path)
16
- return devenv_config.get("devenv", "coderoot", fallback="")
16
+ return os.path.expanduser(devenv_config.get("devenv", "coderoot", fallback=""))
17
17
  except (FileNotFoundError, NoSectionError, NoOptionError):
18
18
  # TODO: Handle the case where there is no config file or the coderoot is not set
19
19
  raise Exception("Failed to read code root from config")
@@ -35,17 +35,26 @@ class State:
35
35
  )
36
36
  self.conn.commit()
37
37
 
38
- def add_started_service(self, service_name: str, mode: str) -> None:
38
+ def update_started_service(self, service_name: str, mode: str) -> None:
39
39
  cursor = self.conn.cursor()
40
40
  started_services = self.get_started_services()
41
- if service_name in started_services:
41
+ active_modes = self.get_active_modes_for_service(service_name)
42
+ if service_name in started_services and mode in active_modes:
42
43
  return
43
- cursor.execute(
44
- """
45
- INSERT INTO started_services (service_name, mode) VALUES (?, ?)
46
- """,
47
- (service_name, mode),
48
- )
44
+ if service_name in started_services:
45
+ cursor.execute(
46
+ """
47
+ UPDATE started_services SET mode = ? WHERE service_name = ?
48
+ """,
49
+ (",".join(active_modes + [mode]), service_name),
50
+ )
51
+ else:
52
+ cursor.execute(
53
+ """
54
+ INSERT INTO started_services (service_name, mode) VALUES (?, ?)
55
+ """,
56
+ (service_name, ",".join(active_modes + [mode])),
57
+ )
49
58
  self.conn.commit()
50
59
 
51
60
  def remove_started_service(self, service_name: str) -> None:
@@ -67,7 +76,7 @@ class State:
67
76
  )
68
77
  return [row[0] for row in cursor.fetchall()]
69
78
 
70
- def get_mode_for_service(self, service_name: str) -> str | None:
79
+ def get_active_modes_for_service(self, service_name: str) -> list[str]:
71
80
  cursor = self.conn.cursor()
72
81
  cursor.execute(
73
82
  """
@@ -77,8 +86,8 @@ class State:
77
86
  )
78
87
  result = cursor.fetchone()
79
88
  if result is None:
80
- return None
81
- return str(result[0])
89
+ return []
90
+ return str(result[0]).split(",")
82
91
 
83
92
  def clear_state(self) -> None:
84
93
  cursor = self.conn.cursor()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -38,9 +38,11 @@ testing/utils.py
38
38
  tests/__init__.py
39
39
  tests/conftest.py
40
40
  tests/commands/test_down.py
41
+ tests/commands/test_list_dependencies.py
41
42
  tests/commands/test_list_services.py
42
43
  tests/commands/test_logs.py
43
44
  tests/commands/test_purge.py
45
+ tests/commands/test_status.py
44
46
  tests/commands/test_up.py
45
47
  tests/commands/test_update.py
46
48
  tests/configs/test_service_config.py
@@ -48,4 +50,5 @@ tests/utils/test_dependencies.py
48
50
  tests/utils/test_docker.py
49
51
  tests/utils/test_docker_compose.py
50
52
  tests/utils/test_install_binary.py
53
+ tests/utils/test_services.py
51
54
  tests/utils/test_state.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -13,6 +13,8 @@ from devservices.constants import CONFIG_FILE_NAME
13
13
  from devservices.constants import DEPENDENCY_CONFIG_VERSION
14
14
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
15
15
  from devservices.constants import DEVSERVICES_DIR_NAME
16
+ from devservices.exceptions import ConfigError
17
+ from devservices.exceptions import ServiceNotFoundError
16
18
  from devservices.utils.state import State
17
19
  from testing.utils import create_config_file
18
20
 
@@ -64,7 +66,7 @@ def test_down_simple(
64
66
  "devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
65
67
  ):
66
68
  state = State()
67
- state.add_started_service("example-service", "default")
69
+ state.update_started_service("example-service", "default")
68
70
  down(args)
69
71
 
70
72
  # Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
@@ -135,7 +137,7 @@ def test_down_error(
135
137
 
136
138
  with mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
137
139
  state = State()
138
- state.add_started_service("example-service", "default")
140
+ state.update_started_service("example-service", "default")
139
141
  with pytest.raises(SystemExit):
140
142
  down(args)
141
143
 
@@ -200,7 +202,7 @@ def test_down_mode_simple(
200
202
  "devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
201
203
  ):
202
204
  state = State()
203
- state.add_started_service("example-service", "test")
205
+ state.update_started_service("example-service", "test")
204
206
  down(args)
205
207
 
206
208
  # Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
@@ -231,3 +233,33 @@ def test_down_mode_simple(
231
233
 
232
234
  captured = capsys.readouterr()
233
235
  assert "Stopping redis" in captured.out.strip()
236
+
237
+
238
+ @mock.patch("devservices.commands.down.find_matching_service")
239
+ def test_down_config_error(
240
+ find_matching_service_mock: mock.Mock, capsys: pytest.CaptureFixture[str]
241
+ ) -> None:
242
+ find_matching_service_mock.side_effect = ConfigError("Config error")
243
+ args = Namespace(service_name="example-service", debug=False)
244
+
245
+ with pytest.raises(SystemExit):
246
+ down(args)
247
+
248
+ find_matching_service_mock.assert_called_once_with("example-service")
249
+ captured = capsys.readouterr()
250
+ assert "Config error" in captured.out.strip()
251
+
252
+
253
+ @mock.patch("devservices.commands.down.find_matching_service")
254
+ def test_down_service_not_found_error(
255
+ find_matching_service_mock: mock.Mock, capsys: pytest.CaptureFixture[str]
256
+ ) -> None:
257
+ find_matching_service_mock.side_effect = ServiceNotFoundError("Service not found")
258
+ args = Namespace(service_name="example-service", debug=False)
259
+
260
+ with pytest.raises(SystemExit):
261
+ down(args)
262
+
263
+ find_matching_service_mock.assert_called_once_with("example-service")
264
+ captured = capsys.readouterr()
265
+ assert "Service not found" in captured.out.strip()
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import Namespace
4
+ from pathlib import Path
5
+ from unittest import mock
6
+
7
+ import pytest
8
+
9
+ from devservices.commands.list_dependencies import list_dependencies
10
+ from devservices.configs.service_config import Dependency
11
+ from devservices.configs.service_config import ServiceConfig
12
+ from devservices.exceptions import ConfigValidationError
13
+ from devservices.exceptions import ServiceNotFoundError
14
+ from devservices.utils.services import Service
15
+
16
+
17
+ @mock.patch("devservices.commands.list_dependencies.find_matching_service")
18
+ def test_list_dependencies_service_not_found(
19
+ mock_find_matching_service: mock.Mock,
20
+ capsys: pytest.CaptureFixture[str],
21
+ ) -> None:
22
+ args = Namespace(service_name="nonexistent-service")
23
+ mock_find_matching_service.side_effect = ServiceNotFoundError(
24
+ "Service nonexistent-service not found"
25
+ )
26
+
27
+ with pytest.raises(SystemExit) as exc_info:
28
+ list_dependencies(args)
29
+
30
+ assert exc_info.value.code == 1
31
+
32
+ mock_find_matching_service.assert_called_once_with("nonexistent-service")
33
+ captured = capsys.readouterr()
34
+ assert "Service nonexistent-service not found" in captured.out
35
+
36
+
37
+ @mock.patch("devservices.commands.list_dependencies.find_matching_service")
38
+ def test_list_dependencies_config_error(
39
+ mock_find_matching_service: mock.Mock,
40
+ capsys: pytest.CaptureFixture[str],
41
+ ) -> None:
42
+ args = Namespace(service_name="test-service")
43
+ mock_find_matching_service.side_effect = ConfigValidationError(
44
+ "Version is required in service config"
45
+ )
46
+
47
+ with pytest.raises(SystemExit) as exc_info:
48
+ list_dependencies(args)
49
+
50
+ assert exc_info.value.code == 1
51
+
52
+ mock_find_matching_service.assert_called_once_with("test-service")
53
+ captured = capsys.readouterr()
54
+ assert "Version is required in service config" in captured.out
55
+
56
+
57
+ @mock.patch("devservices.commands.list_dependencies.find_matching_service")
58
+ def test_list_dependencies_no_dependencies(
59
+ mock_find_matching_service: mock.Mock,
60
+ capsys: pytest.CaptureFixture[str],
61
+ tmp_path: Path,
62
+ ) -> None:
63
+ args = Namespace(service_name="test-service")
64
+ service = Service(
65
+ name="test-service",
66
+ repo_path=str(tmp_path),
67
+ config=ServiceConfig(
68
+ version=0.1,
69
+ service_name="test-service",
70
+ dependencies={},
71
+ modes={"default": []},
72
+ ),
73
+ )
74
+ mock_find_matching_service.return_value = service
75
+
76
+ list_dependencies(args)
77
+
78
+ mock_find_matching_service.assert_called_once_with("test-service")
79
+ captured = capsys.readouterr()
80
+ assert "No dependencies found for test-service" in captured.out
81
+
82
+
83
+ @mock.patch("devservices.commands.list_dependencies.find_matching_service")
84
+ def test_list_dependencies_with_dependencies(
85
+ mock_find_matching_service: mock.Mock,
86
+ capsys: pytest.CaptureFixture[str],
87
+ tmp_path: Path,
88
+ ) -> None:
89
+ args = Namespace(service_name="test-service")
90
+ service = Service(
91
+ name="test-service",
92
+ repo_path=str(tmp_path),
93
+ config=ServiceConfig(
94
+ version=0.1,
95
+ service_name="test-service",
96
+ dependencies={
97
+ "redis": Dependency(description="Redis"),
98
+ "postgres": Dependency(description="Postgres"),
99
+ },
100
+ modes={"default": ["redis", "postgres"]},
101
+ ),
102
+ )
103
+ mock_find_matching_service.return_value = service
104
+
105
+ list_dependencies(args)
106
+
107
+ mock_find_matching_service.assert_called_once_with("test-service")
108
+ captured = capsys.readouterr()
109
+ assert "Dependencies of test-service:" in captured.out
110
+ assert "- redis: Redis" in captured.out
111
+ assert "- postgres: Postgres" in captured.out
@@ -19,7 +19,7 @@ def test_list_running_services(
19
19
  return_value=str(tmp_path / "code"),
20
20
  ), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
21
21
  state = State()
22
- state.add_started_service("example-service", "default")
22
+ state.update_started_service("example-service", "default")
23
23
  config = {
24
24
  "x-sentry-service-config": {
25
25
  "version": 0.1,
@@ -47,7 +47,7 @@ def test_list_running_services(
47
47
 
48
48
  assert (
49
49
  captured.out
50
- == f"Running services:\n- example-service\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
50
+ == f"Running services:\n- example-service\n modes: ['default']\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
51
51
  )
52
52
 
53
53
 
@@ -57,7 +57,7 @@ def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -
57
57
  return_value=str(tmp_path / "code"),
58
58
  ), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
59
59
  state = State()
60
- state.add_started_service("example-service", "default")
60
+ state.update_started_service("example-service", "default")
61
61
  config = {
62
62
  "x-sentry-service-config": {
63
63
  "version": 0.1,
@@ -85,5 +85,5 @@ def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -
85
85
 
86
86
  assert (
87
87
  captured.out
88
- == f"Services installed locally:\n- example-service\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
88
+ == f"Services installed locally:\n- example-service\n modes: ['default']\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
89
89
  )