Skip to content

Commit 95d138c

Browse files
authored
feat(scratch): Make scratch init faster (#1499)
1 parent de9b447 commit 95d138c

2 files changed

Lines changed: 68 additions & 30 deletions

File tree

src/blueapi/cli/scratch.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import stat
55
import textwrap
6+
from concurrent.futures import ThreadPoolExecutor, as_completed
67
from pathlib import Path
78
from subprocess import Popen
89

@@ -46,10 +47,27 @@ def setup_scratch(
4647
That is to prevent namespace clashing with the blueapi application.
4748
""")
4849
)
49-
for repo in config.repositories:
50-
local_directory = config.root / repo.name
51-
ensure_repo(repo.remote_url, local_directory, repo.target_revision)
52-
scratch_install(local_directory, timeout=install_timeout)
50+
51+
with ThreadPoolExecutor() as executor:
52+
futures = [
53+
executor.submit(
54+
ensure_repo,
55+
repo.remote_url,
56+
config.root / repo.name,
57+
repo.target_revision,
58+
)
59+
for repo in config.repositories
60+
]
61+
for future in as_completed(futures):
62+
try:
63+
future.result()
64+
except Exception as exc:
65+
raise RuntimeError("Failed to clone repositories") from exc
66+
67+
scratch_install(
68+
*(config.root / repo.name for repo in config.repositories),
69+
timeout=install_timeout,
70+
)
5371

5472

5573
def ensure_repo(
@@ -69,7 +87,12 @@ def ensure_repo(
6987

7088
if not local_directory.exists():
7189
LOGGER.info(f"Cloning {remote_url}")
72-
Repo.clone_from(remote_url, local_directory, branch=target_revision)
90+
Repo.clone_from(
91+
remote_url,
92+
local_directory,
93+
branch=target_revision,
94+
filter="blob:none",
95+
)
7396
LOGGER.info(f"Cloned {remote_url} -> {local_directory}")
7497
elif local_directory.is_dir():
7598
repo = Repo(local_directory)
@@ -93,34 +116,36 @@ def ensure_repo(
93116
)
94117

95118

96-
def scratch_install(path: Path, timeout: float = _DEFAULT_INSTALL_TIMEOUT) -> None:
119+
def scratch_install(*paths: Path, timeout: float = _DEFAULT_INSTALL_TIMEOUT) -> None:
97120
"""
98-
Install a scratch package. Make blueapi aware of a repository checked out in
99-
the scratch area. Make it automatically follow code changes to that repository
100-
(pending a restart). Do not install any of the package's dependencies as they
121+
Install scratch packages. Make blueapi aware of repositories checked out in
122+
the scratch area. Make it automatically follow code changes to those repositories
123+
(pending a restart). Do not install any of the packages' dependencies as they
101124
may conflict with each other.
102125
103126
Args:
104-
path: Path to the checked out repository
127+
paths: List of Paths to the checked out repositories
105128
timeout: Time to wait for installation subprocess
106129
"""
130+
if not paths:
131+
return
132+
args = [
133+
"uv",
134+
"pip",
135+
"install",
136+
"--no-deps",
137+
]
138+
for path in paths:
139+
_validate_directory(path)
140+
args.extend(["-e", str(path)])
107141

108-
_validate_directory(path)
109-
110-
LOGGER.info(f"Installing {path}")
111-
process = Popen(
112-
[
113-
"uv",
114-
"pip",
115-
"install",
116-
"--no-deps",
117-
"-e",
118-
str(path),
119-
]
120-
)
142+
LOGGER.info("Installing packages")
143+
process = Popen(args)
121144
process.wait(timeout=timeout)
122145
if process.returncode != 0:
123-
raise RuntimeError(f"Failed to install {path}: Exit Code: {process.returncode}")
146+
raise RuntimeError(
147+
f"Failed to install packages: Exit Code: {process.returncode}"
148+
)
124149

125150

126151
def _validate_root_directory(root_path: Path, required_gid: int | None) -> None:

tests/unit_tests/cli/test_scratch.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ def test_repo_cloned_if_not_found_locally(
123123
ensure_repo("http://example.com/foo.git", nonexistant_path)
124124
mock_repo.assert_not_called()
125125
mock_repo.clone_from.assert_called_once_with(
126-
"http://example.com/foo.git", nonexistant_path, branch=None
126+
"http://example.com/foo.git",
127+
nonexistant_path,
128+
branch=None,
129+
filter="blob:none",
127130
)
128131

129132

@@ -140,7 +143,9 @@ def write_repo_files():
140143
with file_path.open("w") as stream:
141144
stream.write("foo")
142145

143-
mock_repo.clone_from.side_effect = lambda url, path, branch=None: write_repo_files()
146+
mock_repo.clone_from.side_effect = lambda url, path, branch, filter: (
147+
write_repo_files()
148+
)
144149

145150
ensure_repo("http://example.com/foo.git", repo_root)
146151
assert file_path.exists()
@@ -163,7 +168,10 @@ def test_cloned_repo_changes_to_new_branch(mock_repo, directory_path: Path):
163168
ensure_repo("http://example.com/foo.git", directory_path / "demo_branch", "demo")
164169

165170
mock_repo.clone_from.assert_called_once_with(
166-
"http://example.com/foo.git", ANY, branch="demo"
171+
"http://example.com/foo.git",
172+
ANY,
173+
branch="demo",
174+
filter="blob:none",
167175
)
168176

169177

@@ -345,8 +353,11 @@ def test_setup_scratch_iterates_repos(
345353

346354
mock_scratch_install.assert_has_calls(
347355
[
348-
call(directory_path_with_sgid / "foo", timeout=120.0),
349-
call(directory_path_with_sgid / "bar", timeout=120.0),
356+
call(
357+
directory_path_with_sgid / "foo",
358+
directory_path_with_sgid / "bar",
359+
timeout=120.0,
360+
),
350361
]
351362
)
352363

@@ -376,7 +387,9 @@ def test_setup_scratch_continues_after_failure(
376387
],
377388
)
378389
mock_ensure_repo.side_effect = [None, RuntimeError("bar"), None]
379-
with pytest.raises(RuntimeError, match="bar"):
390+
with pytest.raises(
391+
RuntimeError, match="Failed to clone", check=lambda e: str(e.__cause__) == "bar"
392+
):
380393
setup_scratch(config)
381394

382395

0 commit comments

Comments
 (0)