sentry-devenv 1.18.0__tar.gz → 1.20.0__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 (83) hide show
  1. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/PKG-INFO +1 -1
  2. sentry_devenv-1.18.0/devenv/checks/credsStore.py → sentry_devenv-1.20.0/devenv/checks/dockerConfig.py +11 -1
  3. sentry_devenv-1.20.0/devenv/checks/dockerDesktop.py +47 -0
  4. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/checks/limaDns.py +2 -2
  5. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/doctor.py +9 -9
  6. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/archive.py +53 -44
  7. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/colima.py +1 -1
  8. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/docker.py +2 -2
  9. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/gcloud.py +1 -1
  10. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/limactl.py +1 -1
  11. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/node.py +2 -2
  12. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/tenv.py +1 -1
  13. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/venv.py +1 -1
  14. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/pyproject.toml +1 -1
  15. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/PKG-INFO +1 -1
  16. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/SOURCES.txt +3 -2
  17. sentry_devenv-1.18.0/tests/checks/test_credStore.py → sentry_devenv-1.20.0/tests/checks/test_dockerConfig.py +12 -6
  18. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/test_run_checks.py +1 -1
  19. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_archive.py +59 -2
  20. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/README.md +0 -0
  21. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/ci/integration/repo/devenv/sync.py +0 -0
  22. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/__init__.py +0 -0
  23. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/__main__.py +0 -0
  24. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/bootstrap.py +0 -0
  25. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/checks/__init__.py +0 -0
  26. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/checks/test.py +0 -0
  27. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/colima.py +0 -0
  28. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/constants.py +0 -0
  29. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/fetch.py +0 -0
  30. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/__init__.py +0 -0
  31. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/brew.py +0 -0
  32. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/config.py +0 -0
  33. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/context.py +0 -0
  34. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/direnv.py +0 -0
  35. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/fs.py +0 -0
  36. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/github.py +0 -0
  37. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/modules.py +0 -0
  38. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/proc.py +0 -0
  39. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/repository.py +0 -0
  40. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib/rosetta.py +0 -0
  41. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib_check/__init__.py +0 -0
  42. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib_check/brew.py +0 -0
  43. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/lib_check/types.py +0 -0
  44. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/main.py +0 -0
  45. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/pin_gha.py +0 -0
  46. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/py.typed +0 -0
  47. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/pythons.py +0 -0
  48. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/sync.py +0 -0
  49. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/devenv/update.py +0 -0
  50. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/dependency_links.txt +0 -0
  51. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/entry_points.txt +0 -0
  52. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/requires.txt +0 -0
  53. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/sentry_devenv.egg-info/top_level.txt +0 -0
  54. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/setup.cfg +0 -0
  55. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/__init__.py +0 -0
  56. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/checks/__init__.py +0 -0
  57. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/conftest.py +0 -0
  58. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/__init__.py +0 -0
  59. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/bad_check.py +0 -0
  60. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/bad_fix.py +0 -0
  61. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/broken_check.py +0 -0
  62. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/broken_fix.py +0 -0
  63. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/failing_check.py +0 -0
  64. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/failing_check_with_msg.py +0 -0
  65. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/no_check.py +0 -0
  66. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/no_name.py +0 -0
  67. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/no_tags.py +0 -0
  68. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/devenv/checks/passing_check.py +0 -0
  69. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/test_attempt_fix.py +0 -0
  70. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/test_filter_failing_checks.py +0 -0
  71. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/test_load_checks.py +0 -0
  72. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/doctor/test_prompt_for_fix.py +0 -0
  73. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_brew.py +0 -0
  74. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_direnv.py +0 -0
  75. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_fs.py +0 -0
  76. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_github.py +0 -0
  77. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_proc.py +0 -0
  78. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_repository.py +0 -0
  79. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/lib/test_venv.py +0 -0
  80. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/test_main.py +0 -0
  81. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/test_pythons.py +0 -0
  82. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/test_sync.py +0 -0
  83. {sentry_devenv-1.18.0 → sentry_devenv-1.20.0}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentry_devenv
3
- Version: 1.18.0
3
+ Version: 1.20.0
4
4
  Summary: Utilities for setting up a Sentry development environment
5
5
  Author-email: Joshua Li <joshua.li@sentry.io>, Ian Woodard <ian.woodard@sentry.io>, Buck Evan <buck.evan@sentry.io>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -8,7 +8,7 @@ from devenv.lib_check.types import checker
8
8
  from devenv.lib_check.types import fixer
9
9
 
10
10
  tags: set[str] = {"builtin"}
11
- name = "credsStore"
11
+ name = "correct docker configuration"
12
12
 
13
13
 
14
14
  @checker
@@ -23,6 +23,15 @@ def check() -> tuple[bool, str]:
23
23
  if store and not shutil.which(f"docker-credential-{store}"):
24
24
  return False, "credsStore requires nonexistent binary"
25
25
 
26
+ # When docker-buildx is installed via brew, brew adds cliPluginsExtraDirs
27
+ # which takes precedence over the default plugin path we rely on.
28
+ # This ensures the devenv-managed global docker cli uses the default plugin path.
29
+ if config.get("cliPluginsExtraDirs"):
30
+ return (
31
+ False,
32
+ "cliPluginsExtraDirs exists, which overshadows the default plugin path",
33
+ )
34
+
26
35
  return True, ""
27
36
 
28
37
 
@@ -33,6 +42,7 @@ def fix() -> tuple[bool, str]:
33
42
  config = json.load(f)
34
43
 
35
44
  config.pop("credsStore", None)
45
+ config.pop("cliPluginsExtraDirs", None)
36
46
 
37
47
  with open(os.path.expanduser("~/.docker/config.json"), "w") as f:
38
48
  json.dump(config, f)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from devenv.lib import proc
6
+ from devenv.lib_check.types import checker
7
+ from devenv.lib_check.types import fixer
8
+
9
+ tags: set[str] = {"builtin"}
10
+ name = "docker desktop shouldn't be running"
11
+
12
+
13
+ def docker_desktop_is_running() -> bool:
14
+ procs = proc.run(("/bin/ps", "-Ac", "-o", "comm"), stdout=True)
15
+ return "Docker Desktop" in procs.split("\n")
16
+
17
+
18
+ @checker
19
+ def check() -> tuple[bool, str]:
20
+ if docker_desktop_is_running():
21
+ return (
22
+ False,
23
+ "Docker Desktop is running. We don't support it, and it conflicts with colima.",
24
+ )
25
+
26
+ return True, ""
27
+
28
+
29
+ @fixer
30
+ def fix() -> tuple[bool, str]:
31
+ # regular pkill won't stop the Docker Desktop UI,
32
+ # it'll just spin. the proper way to terminate it
33
+ # without SIGKILL is to use osascript.
34
+ print("Attempting to stop Docker Desktop.")
35
+ try:
36
+ proc.run(("osascript", "-e", 'quit app "Docker Desktop"'))
37
+ except RuntimeError as e:
38
+ return False, f"failed to quit Docker Desktop:\n{e}\n"
39
+
40
+ # osascript doesn't wait to make sure it finishes quitting
41
+ # so let's block for up to 5 secs
42
+ for _ in range(10):
43
+ time.sleep(0.5)
44
+ if not docker_desktop_is_running():
45
+ return True, ""
46
+
47
+ return False, "Docker Desktop is taking too long to quit... try again?"
@@ -8,7 +8,7 @@ from devenv.lib_check.types import checker
8
8
  from devenv.lib_check.types import fixer
9
9
 
10
10
  tags: set[str] = {"builtin"}
11
- name = "limaDns"
11
+ name = "colima's DNS isn't working"
12
12
 
13
13
 
14
14
  @checker
@@ -19,7 +19,7 @@ def check() -> tuple[bool, str]:
19
19
 
20
20
  status = colima.check()
21
21
  if status != colima.ColimaStatus.UP:
22
- return False, "colima isn't up"
22
+ return False, "Colima isn't running."
23
23
 
24
24
  try:
25
25
  proc.run(
@@ -143,7 +143,7 @@ def run_checks(
143
143
  results: dict[Check, tuple[bool, str]] = {}
144
144
  for check in checks:
145
145
  if check in skip:
146
- print(f"\t⏭️ Skipped {check.name}".expandtabs(4))
146
+ print(f" ⏭️ Skipped {check.name}")
147
147
  continue
148
148
  futures[check] = executor.submit(check.check)
149
149
  for check, future in futures.items():
@@ -162,9 +162,9 @@ def filter_failing_checks(
162
162
  for check, result in results.items():
163
163
  ok, msg = result
164
164
  if ok:
165
- print(f"\t✅ check: {check.name}".expandtabs(4))
165
+ print(f" ✅ check: {check.name}")
166
166
  continue
167
- print(f"\t❌ check: {check.name}{msg}".expandtabs(4))
167
+ print(f" ❌ check: {check.name}\n {msg}")
168
168
  failing_checks.append(check)
169
169
  return failing_checks
170
170
 
@@ -172,7 +172,7 @@ def filter_failing_checks(
172
172
  def prompt_for_fix(check: Check) -> bool:
173
173
  """Prompt the user to attempt a fix."""
174
174
  return input(
175
- f"\t\tDo you want to attempt to fix {check.name}? (Y/n): ".expandtabs(4)
175
+ f" Do you want to attempt to fix {check.name}? (Y/n): "
176
176
  ).lower() in {"y", "yes", ""}
177
177
 
178
178
 
@@ -229,19 +229,19 @@ def main(context: Context, argv: Sequence[str] | None = None) -> int:
229
229
  if args.check_only:
230
230
  return 1
231
231
 
232
- print("\nThe following problems have been identified:")
233
232
  skip: list[Check] = []
233
+ print("\nLet's go through the failures one by one.")
234
234
  for check in failing_checks:
235
- print(f"\t❌ {check.name}".expandtabs(4))
235
+ print(f"❌ {check.name}")
236
236
  # Prompt for fixes one by one, so the user can decide to skip a fix.
237
237
  if prompt_for_fix(check):
238
238
  ok, msg = attempt_fix(check, executor)
239
239
  if ok:
240
- print(f"\t\t✅ fix: {check.name}".expandtabs(4))
240
+ print(f"✅ fix: {check.name}")
241
241
  else:
242
- print(f"\t\t❌ fix: {check.name}{msg}".expandtabs(4))
242
+ print(f"❌ fix: {check.name}{msg}")
243
243
  else:
244
- print(f"\t\t⏭️ Skipping {check.name}".expandtabs(4))
244
+ print(f" ⏭️ Skipping {check.name}")
245
245
  skip.append(check)
246
246
 
247
247
  print("\nChecking that fixes worked as expected...")
@@ -38,51 +38,60 @@ def download(
38
38
  dest = f"{cache_root}/{sha256}"
39
39
  os.makedirs(cache_root, exist_ok=True)
40
40
 
41
- if not os.path.exists(dest):
42
- headers = {}
43
- if url.startswith("https://ghcr.io/v2/homebrew"):
44
- # downloading homebrew blobs requires auth
45
- # you can get an anonymous token from https://ghcr.io/token?service=ghcr.io&scope=repository%3Ahomebrew/core/go%3Apull
46
- # but there's also a special shortcut token QQ==
47
- # https://github.com/Homebrew/brew/blob/2184406bd8444e4de2626f5b0c749d4d08cb1aed/Library/Homebrew/brew.sh#L993
48
- headers["Authorization"] = "bearer QQ=="
49
-
50
- req = urllib.request.Request(url, headers=headers)
51
-
52
- retry_sleep = 1.0
53
- while retries >= 0:
54
- try:
55
- resp = urllib.request.urlopen(req)
56
- break
57
- except HTTPError as e:
58
- if retries == 0:
59
- raise RuntimeError(f"Error getting {url}: {e}")
60
- print(f"Error getting {url} ({retries} retries left): {e}")
61
-
62
- time.sleep(retry_sleep)
63
- retries -= 1
64
- retry_sleep *= retry_exp
65
-
66
- dest_dir = os.path.dirname(dest)
67
- os.makedirs(dest_dir, exist_ok=True)
68
-
69
- with tempfile.NamedTemporaryFile(delete=False, dir=dest_dir) as tmpf:
70
- shutil.copyfileobj(resp, tmpf)
71
- tmpf.seek(0)
72
- checksum = hashlib.sha256()
41
+ if os.path.islink(dest):
42
+ # there are cases where dest can be an existing symlink
43
+ # (docker desktop starts and puts symlinks into ~/.docker/cli-plugins)
44
+ # such symlinks should be removed otherwise callers to download
45
+ # usually try to chmod after and end up with PermissionError
46
+ os.remove(dest)
47
+
48
+ if os.path.exists(dest):
49
+ return dest
50
+
51
+ headers = {}
52
+ if url.startswith("https://ghcr.io/v2/homebrew"):
53
+ # downloading homebrew blobs requires auth
54
+ # you can get an anonymous token from https://ghcr.io/token?service=ghcr.io&scope=repository%3Ahomebrew/core/go%3Apull
55
+ # but there's also a special shortcut token QQ==
56
+ # https://github.com/Homebrew/brew/blob/2184406bd8444e4de2626f5b0c749d4d08cb1aed/Library/Homebrew/brew.sh#L993
57
+ headers["Authorization"] = "bearer QQ=="
58
+
59
+ req = urllib.request.Request(url, headers=headers)
60
+
61
+ retry_sleep = 1.0
62
+ while retries >= 0:
63
+ try:
64
+ resp = urllib.request.urlopen(req)
65
+ break
66
+ except HTTPError as e:
67
+ if retries == 0:
68
+ raise RuntimeError(f"Error getting {url}: {e}")
69
+ print(f"Error getting {url} ({retries} retries left): {e}")
70
+
71
+ time.sleep(retry_sleep)
72
+ retries -= 1
73
+ retry_sleep *= retry_exp
74
+
75
+ dest_dir = os.path.dirname(dest)
76
+ os.makedirs(dest_dir, exist_ok=True)
77
+
78
+ with tempfile.NamedTemporaryFile(delete=False, dir=dest_dir) as tmpf:
79
+ shutil.copyfileobj(resp, tmpf)
80
+ tmpf.seek(0)
81
+ checksum = hashlib.sha256()
82
+ buf = tmpf.read(4096)
83
+ while buf:
84
+ checksum.update(buf)
73
85
  buf = tmpf.read(4096)
74
- while buf:
75
- checksum.update(buf)
76
- buf = tmpf.read(4096)
77
-
78
- if not secrets.compare_digest(checksum.hexdigest(), sha256):
79
- raise RuntimeError(
80
- f"checksum mismatch for {url}:\n"
81
- f"- got: {checksum.hexdigest()}\n"
82
- f"- expected: {sha256}\n"
83
- )
84
-
85
- atomic_replace(tmpf.name, dest)
86
+
87
+ if not secrets.compare_digest(checksum.hexdigest(), sha256):
88
+ raise RuntimeError(
89
+ f"checksum mismatch for {url}:\n"
90
+ f"- got: {checksum.hexdigest()}\n"
91
+ f"- expected: {sha256}\n"
92
+ )
93
+
94
+ atomic_replace(tmpf.name, dest)
86
95
 
87
96
  return dest
88
97
 
@@ -78,7 +78,7 @@ def install_global() -> None:
78
78
  installed_version = stdout.strip().split()[-1]
79
79
  if version == installed_version:
80
80
  return
81
- print(f"installed colima {installed_version} is outdated!")
81
+ print(f"installed colima {installed_version} is unexpected!")
82
82
 
83
83
  print(f"installing colima {version}...")
84
84
  uninstall(binroot)
@@ -101,7 +101,7 @@ def _check_buildx(binroot: str, expected_version: str) -> bool:
101
101
  return True
102
102
 
103
103
  print(
104
- f"installed docker-buildx {installed_version} is outdated! expected: {expected_version}"
104
+ f"installed docker-buildx {installed_version} is unexpected! expected: {expected_version}"
105
105
  )
106
106
  return False
107
107
 
@@ -133,7 +133,7 @@ def install_global() -> None:
133
133
  stdout = proc.run((f"{binroot}/docker", "--version"), stdout=True)
134
134
  installed_version = stdout.strip().split()[2][:-1]
135
135
  if version != installed_version:
136
- print(f"installed docker {installed_version} is outdated!")
136
+ print(f"installed docker {installed_version} is unexpected!")
137
137
  print(f"installing docker (cli, not desktop) {version}...")
138
138
  uninstall(binroot)
139
139
  _install(
@@ -82,7 +82,7 @@ def install(version: str, url: str, sha256: str, reporoot: str) -> None:
82
82
  installed_version = f.read().strip()
83
83
  if version == installed_version:
84
84
  return
85
- print(f"installed gcloud {installed_version} is outdated!")
85
+ print(f"installed gcloud {installed_version} is unexpected!")
86
86
 
87
87
  print(f"installing gcloud {version}...")
88
88
  uninstall(binroot)
@@ -91,7 +91,7 @@ def install_global() -> None:
91
91
  installed_version = stdout.strip().split()[-1]
92
92
  if version == installed_version:
93
93
  return
94
- print(f"installed limactl {installed_version} is outdated!")
94
+ print(f"installed limactl {installed_version} is unexpected!")
95
95
 
96
96
  uninstall(binroot)
97
97
  _install(cfg[SYSTEM_MACHINE], cfg[f"{SYSTEM_MACHINE}_sha256"], binroot)
@@ -67,7 +67,7 @@ def installed(version: str, binroot: str) -> bool:
67
67
  if version == installed_version:
68
68
  return True
69
69
 
70
- print(f"installed node {installed_version} is outdated!")
70
+ print(f"installed node {installed_version} is unexpected!")
71
71
  return False
72
72
 
73
73
 
@@ -118,7 +118,7 @@ def installed_yarn(version: str, binroot: str) -> bool:
118
118
  if version == installed_version:
119
119
  return True
120
120
 
121
- print(f"installed yarn {installed_version} is outdated!")
121
+ print(f"installed yarn {installed_version} is unexpected!")
122
122
  return False
123
123
 
124
124
 
@@ -88,7 +88,7 @@ def install(version: str, url: str, sha256: str, reporoot: str) -> None:
88
88
  installed_version = _version(binpath)
89
89
  if version == installed_version:
90
90
  return
91
- print(f"installed tenv {installed_version} is outdated!")
91
+ print(f"installed tenv {installed_version} is unexpected!")
92
92
 
93
93
  print(f"installing tenv {version}...")
94
94
  uninstall(binroot)
@@ -106,7 +106,7 @@ def ensure(venv: str, python_version: str, url: str, sha256: str) -> None:
106
106
  return
107
107
 
108
108
  print(
109
- f"virtualenv doesn't exist or is using an outdated python, recreating at {venv}..."
109
+ f"virtualenv doesn't exist or is using an unexpected python, recreating at {venv}..."
110
110
  )
111
111
  if os.path.exists(venv):
112
112
  shutil.rmtree(venv)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sentry_devenv"
7
- version = "1.18.0"
7
+ version = "1.20.0"
8
8
  authors = [
9
9
  { name="Joshua Li", email="joshua.li@sentry.io" },
10
10
  { name="Ian Woodard", email="ian.woodard@sentry.io" },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentry_devenv
3
- Version: 1.18.0
3
+ Version: 1.20.0
4
4
  Summary: Utilities for setting up a Sentry development environment
5
5
  Author-email: Joshua Li <joshua.li@sentry.io>, Ian Woodard <ian.woodard@sentry.io>, Buck Evan <buck.evan@sentry.io>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -16,7 +16,8 @@ devenv/pythons.py
16
16
  devenv/sync.py
17
17
  devenv/update.py
18
18
  devenv/checks/__init__.py
19
- devenv/checks/credsStore.py
19
+ devenv/checks/dockerConfig.py
20
+ devenv/checks/dockerDesktop.py
20
21
  devenv/checks/limaDns.py
21
22
  devenv/checks/test.py
22
23
  devenv/lib/__init__.py
@@ -54,7 +55,7 @@ tests/test_pythons.py
54
55
  tests/test_sync.py
55
56
  tests/utils.py
56
57
  tests/checks/__init__.py
57
- tests/checks/test_credStore.py
58
+ tests/checks/test_dockerConfig.py
58
59
  tests/doctor/__init__.py
59
60
  tests/doctor/test_attempt_fix.py
60
61
  tests/doctor/test_filter_failing_checks.py
@@ -8,7 +8,7 @@ from unittest import mock
8
8
 
9
9
  import pytest
10
10
 
11
- from devenv.checks import credsStore
11
+ from devenv.checks import dockerConfig
12
12
 
13
13
 
14
14
  @pytest.fixture
@@ -25,26 +25,32 @@ def fake_config(tmp_path: pathlib.Path) -> Generator[pathlib.Path]:
25
25
 
26
26
  def test_no_credsStore_ok(fake_config: pathlib.Path) -> None:
27
27
  fake_config.write_text("{}")
28
- assert credsStore.check() == (True, "")
28
+ assert dockerConfig.check() == (True, "")
29
29
 
30
30
 
31
31
  def test_binary_ok(fake_config: pathlib.Path) -> None:
32
32
  fake_config.write_text('{"credsStore": "example"}')
33
33
  with mock.patch.object(shutil, "which", return_value="/fake/exe"):
34
- assert credsStore.check() == (True, "")
34
+ assert dockerConfig.check() == (True, "")
35
35
 
36
36
 
37
37
  @pytest.mark.parametrize("name", ("desktop", "osxkeychain"))
38
38
  def test_binary_missing(fake_config: pathlib.Path, name: str) -> None:
39
39
  fake_config.write_text(f'{{"credsStore": "{name}"}}')
40
40
  with mock.patch.object(shutil, "which", return_value=None):
41
- assert credsStore.check() == (
41
+ assert dockerConfig.check() == (
42
42
  False,
43
43
  "credsStore requires nonexistent binary",
44
44
  )
45
45
 
46
46
 
47
- def test_fix(fake_config: pathlib.Path) -> None:
47
+ def test_fix_credsStore(fake_config: pathlib.Path) -> None:
48
48
  fake_config.write_text('{"credsStore": "bad"}')
49
- assert credsStore.fix() == (True, "")
49
+ assert dockerConfig.fix() == (True, "")
50
+ assert fake_config.read_text() == "{}"
51
+
52
+
53
+ def test_fix_cliPluginsExtraDirs(fake_config: pathlib.Path) -> None:
54
+ fake_config.write_text('{"cliPluginsExtraDirs": ["foo/"]}')
55
+ assert dockerConfig.fix() == (True, "")
50
56
  assert fake_config.read_text() == "{}"
@@ -51,7 +51,7 @@ def test_run_checks_skip(capsys: pytest.CaptureFixture[str]) -> None:
51
51
  [first_check, second_check], ThreadPoolExecutor(), skip=[second_check]
52
52
  ) == {first_check: (True, "")}
53
53
  captured = capsys.readouterr()
54
- assert captured.out == " ⏭️ Skipped failing check\n"
54
+ assert captured.out == " ⏭️ Skipped failing check\n"
55
55
 
56
56
 
57
57
  def test_run_checks_multiple_failing_checks() -> None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
+ import os
4
5
  import pathlib
5
6
  import tarfile
6
7
  import time
@@ -169,6 +170,12 @@ def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:
169
170
  )
170
171
 
171
172
  dest = f"{tmp_path}/a"
173
+
174
+ # if dest is already a valid symlink it should be paved over
175
+ with open(f"{tmp_path}/hi", "wb"):
176
+ pass
177
+ os.symlink(f"{tmp_path}/hi", dest)
178
+
172
179
  with mock.patch.object(
173
180
  urllib.request,
174
181
  "urlopen",
@@ -186,7 +193,22 @@ def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:
186
193
  with open(dest, "rb") as f:
187
194
  assert f.read() == data
188
195
 
189
- dest = f"{tmp_path}/b"
196
+
197
+ def test_download_exceeded_retries(
198
+ tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
199
+ ) -> None:
200
+ data_sha256 = (
201
+ "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
202
+ )
203
+
204
+ err = urllib.error.HTTPError(
205
+ "https://example.com/foo",
206
+ 503,
207
+ "Service Unavailable",
208
+ "", # type: ignore
209
+ io.BytesIO(b""),
210
+ )
211
+ dest = f"{tmp_path}/a"
190
212
  with pytest.raises(RuntimeError) as excinfo:
191
213
  with mock.patch.object(
192
214
  urllib.request,
@@ -202,7 +224,42 @@ def test_download(tmp_path: pathlib.Path, mock_sleep: mock.MagicMock) -> None:
202
224
  == "Error getting https://example.com/foo: HTTP Error 503: Service Unavailable"
203
225
  )
204
226
 
205
- dest = f"{tmp_path}/b"
227
+
228
+ def test_download_dest_is_broken_symlink(
229
+ tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
230
+ ) -> None:
231
+ data = b"foo\n"
232
+ data_sha256 = (
233
+ "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
234
+ )
235
+
236
+ dest = f"{tmp_path}/a"
237
+
238
+ # if dest is already a dead symlink it should be paved over as well
239
+ os.symlink(f"{tmp_path}/does-not-exist", dest)
240
+
241
+ with mock.patch.object(
242
+ urllib.request,
243
+ "urlopen",
244
+ autospec=True,
245
+ side_effect=(io.BytesIO(data),),
246
+ ):
247
+ archive.download("https://example.com/foo", data_sha256, dest)
248
+
249
+ with open(dest, "rb") as f:
250
+ assert f.read() == data
251
+
252
+
253
+ def test_download_wrong_sha(
254
+ tmp_path: pathlib.Path, mock_sleep: mock.MagicMock
255
+ ) -> None:
256
+ data = b"foo\n"
257
+ data_sha256 = (
258
+ "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
259
+ )
260
+
261
+ dest = f"{tmp_path}/a"
262
+
206
263
  with pytest.raises(RuntimeError) as excinfo:
207
264
  with mock.patch.object(
208
265
  urllib.request,
File without changes
File without changes