Skip to content

Commit 0f61344

Browse files
authored
Add more tests for prek cache gc (#1425)
1 parent 794d0a6 commit 0f61344

File tree

3 files changed

+219
-67
lines changed

3 files changed

+219
-67
lines changed

crates/prek/src/cli/cache_gc.rs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tracing::{debug, trace, warn};
1111

1212
use crate::cli::ExitStatus;
1313
use crate::cli::cache_size::{dir_size_bytes, human_readable_bytes};
14-
use crate::config::{self, Error as ConfigError, Language, Repo as ConfigRepo, load_config};
14+
use crate::config::{self, Error as ConfigError, Repo as ConfigRepo, load_config};
1515
use crate::hook::{HOOK_MARKER, HookEnvKey, HookSpec, InstallInfo, Repo as HookRepo};
1616
use crate::printer::Printer;
1717
use crate::store::{CacheBucket, REPO_MARKER, Store, ToolBucket};
@@ -193,7 +193,8 @@ pub(crate) async fn cache_gc(
193193

194194
// Mark tools/caches from hook languages.
195195
for key in &used_env_keys {
196-
mark_tools_and_cache_for_language(key.language, &mut used_tools, &mut used_cache);
196+
used_tools.extend(key.language.tool_buckets());
197+
used_cache.extend(key.language.cache_buckets());
197198
}
198199

199200
// Mark hook environments by matching already-installed env metadata.
@@ -400,36 +401,6 @@ fn hook_env_keys_from_config(store: &Store, config: &config::Config) -> Vec<Hook
400401
keys
401402
}
402403

403-
fn mark_tools_and_cache_for_language(
404-
language: Language,
405-
used_tools: &mut FxHashSet<ToolBucket>,
406-
used_cache: &mut FxHashSet<CacheBucket>,
407-
) {
408-
match language {
409-
Language::Python | Language::Pygrep => {
410-
used_tools.insert(ToolBucket::Uv);
411-
used_tools.insert(ToolBucket::Python);
412-
used_cache.insert(CacheBucket::Uv);
413-
used_cache.insert(CacheBucket::Python);
414-
}
415-
Language::Node => {
416-
used_tools.insert(ToolBucket::Node);
417-
}
418-
Language::Golang => {
419-
used_tools.insert(ToolBucket::Go);
420-
used_cache.insert(CacheBucket::Go);
421-
}
422-
Language::Ruby => {
423-
used_tools.insert(ToolBucket::Ruby);
424-
}
425-
Language::Rust => {
426-
used_tools.insert(ToolBucket::Rustup);
427-
used_cache.insert(CacheBucket::Cargo);
428-
}
429-
_ => {}
430-
}
431-
}
432-
433404
fn mark_tool_versions_from_install_info(
434405
store: &Store,
435406
info: &InstallInfo,
@@ -438,16 +409,7 @@ fn mark_tool_versions_from_install_info(
438409
// NOTE: `InstallInfo.toolchain` is typically the executable path (e.g.
439410
// tools/go/1.24.0/bin/go). We keep the first path component under the tool bucket.
440411
// If we can't recognize it, we do nothing (and GC will keep all versions).
441-
let buckets: &[ToolBucket] = match info.language {
442-
Language::Python | Language::Pygrep => &[ToolBucket::Python, ToolBucket::Uv],
443-
Language::Node => &[ToolBucket::Node],
444-
Language::Golang => &[ToolBucket::Go],
445-
Language::Ruby => &[ToolBucket::Ruby],
446-
Language::Rust => &[ToolBucket::Rustup],
447-
_ => &[],
448-
};
449-
450-
for bucket in buckets {
412+
for bucket in info.language.tool_buckets() {
451413
let bucket_root = store.tools_path(*bucket);
452414
if let Some(version) = tool_version_dir_name(&bucket_root, &info.toolchain) {
453415
used_tool_versions
@@ -516,6 +478,7 @@ fn sweep_tool_bucket_versions(
516478
}
517479
};
518480
let path = entry.path();
481+
// Don't remove files (uv, and rustup are files inside tools/).
519482
if !path.is_dir() {
520483
continue;
521484
}
@@ -768,3 +731,66 @@ fn format_dependency_list(deps: &[String], max_items: usize, max_chars: usize) -
768731
}
769732
truncate_end(&rendered, max_chars)
770733
}
734+
735+
#[cfg(test)]
736+
mod tests {
737+
use super::*;
738+
739+
#[test]
740+
fn truncate_end_returns_input_when_short_enough() {
741+
assert_eq!(truncate_end("abc", 3), "abc");
742+
assert_eq!(truncate_end("abc", 10), "abc");
743+
}
744+
745+
#[test]
746+
fn truncate_end_truncates_and_appends_ellipsis() {
747+
assert_eq!(truncate_end("abcd", 3), "ab…");
748+
assert_eq!(truncate_end("abcdef", 5), "abcd…");
749+
}
750+
751+
#[test]
752+
fn truncate_end_counts_chars_not_bytes() {
753+
// 3 unicode scalar values.
754+
assert_eq!(truncate_end("ééé", 3), "ééé");
755+
assert_eq!(truncate_end("ééé", 2), "é…");
756+
}
757+
758+
#[test]
759+
fn split_repo_dependency_prefers_url_like_repo_at_rev() {
760+
let mut deps = FxHashSet::default();
761+
deps.insert("requests==2.32.0".to_string());
762+
deps.insert("black==24.1.0".to_string());
763+
deps.insert("https://github.com/pre-commit/pre-commit-hooks@v1.0.0".to_string());
764+
765+
let (repo_dep, rest) = split_repo_dependency(&deps);
766+
767+
assert_eq!(
768+
repo_dep.as_deref(),
769+
Some("https://github.com/pre-commit/pre-commit-hooks@v1.0.0")
770+
);
771+
assert_eq!(rest, vec!["black==24.1.0", "requests==2.32.0"]);
772+
}
773+
774+
#[test]
775+
fn split_repo_dependency_returns_none_when_no_repo_like_dep() {
776+
let mut deps = FxHashSet::default();
777+
deps.insert("requests==2.32.0".to_string());
778+
deps.insert("black==24.1.0".to_string());
779+
780+
let (repo_dep, rest) = split_repo_dependency(&deps);
781+
assert!(repo_dep.is_none());
782+
assert_eq!(rest, vec!["black==24.1.0", "requests==2.32.0"]);
783+
}
784+
785+
#[test]
786+
fn format_dependency_list_includes_more_suffix() {
787+
let deps = vec!["a".to_string(), "b".to_string(), "c".to_string()];
788+
assert_eq!(format_dependency_list(&deps, 2, 200), "a, b, … (+1 more)");
789+
}
790+
791+
#[test]
792+
fn format_dependency_list_truncates_rendered_string() {
793+
let deps = vec!["abcdef".to_string()];
794+
assert_eq!(format_dependency_list(&deps, 6, 5), "abcd…");
795+
}
796+
}

crates/prek/src/languages/mod.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::config::Language;
1515
use crate::fs::{CWD, Simplified};
1616
use crate::hook::{Hook, InstallInfo, InstalledHook, Repo};
1717
use crate::identify::parse_shebang;
18-
use crate::store::Store;
18+
use crate::store::{CacheBucket, Store, ToolBucket};
1919
use crate::{archive, hooks, warn_user_once};
2020

2121
mod bun;
@@ -144,10 +144,31 @@ impl Language {
144144
pub fn supports_install_env(self) -> bool {
145145
!matches!(
146146
self,
147-
Self::DockerImage | Self::Fail | Self::Pygrep | Self::Script | Self::System
147+
Self::DockerImage | Self::Fail | Self::Script | Self::System
148148
)
149149
}
150150

151+
pub fn tool_buckets(self) -> &'static [ToolBucket] {
152+
match self {
153+
Self::Bun => &[ToolBucket::Bun],
154+
Self::Golang => &[ToolBucket::Go],
155+
Self::Node => &[ToolBucket::Node],
156+
Self::Python | Self::Pygrep => &[ToolBucket::Uv, ToolBucket::Python],
157+
Self::Ruby => &[ToolBucket::Ruby],
158+
Self::Rust => &[ToolBucket::Rustup],
159+
_ => &[],
160+
}
161+
}
162+
163+
pub fn cache_buckets(self) -> &'static [CacheBucket] {
164+
match self {
165+
Self::Golang => &[CacheBucket::Go],
166+
Self::Python | Self::Pygrep => &[CacheBucket::Uv, CacheBucket::Python],
167+
Self::Rust => &[CacheBucket::Cargo],
168+
_ => &[],
169+
}
170+
}
171+
151172
/// Return whether the language allows specifying the version, e.g. we can install a specific
152173
/// requested language version.
153174
/// See <https://pre-commit.com/#overriding-language-version>

crates/prek/tests/cache.rs

Lines changed: 128 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,32 @@ fn cache_gc_verbose_shows_removed_entries() {
4646
home.child("hooks/hook-env-dead")
4747
.create_dir_all()
4848
.expect("create hook env dir");
49+
home.child("hooks/hook-env-dead/.prek-hook.json")
50+
.write_str(
51+
&serde_json::to_string_pretty(&json!({
52+
"language": "python",
53+
"language_version": "3.12.0",
54+
"dependencies": [
55+
"https://example.com/repo@v1.0.0",
56+
"dep1",
57+
"dep2",
58+
"dep3",
59+
"dep4",
60+
"dep5",
61+
"dep6",
62+
"dep7",
63+
],
64+
"env_path": home.child("hooks/hook-env-dead").path(),
65+
"toolchain": "/usr/bin/python3",
66+
"extra": {},
67+
}))
68+
.expect("serialize hook marker"),
69+
)
70+
.expect("write hook marker");
71+
72+
home.child("cache/go")
73+
.create_dir_all()
74+
.expect("create cache dir");
4975

5076
// Have a tracked config that exists but references nothing (so everything above is unreferenced).
5177
let config_path = context.work_dir().child(CONFIG_FILE);
@@ -55,15 +81,22 @@ fn cache_gc_verbose_shows_removed_entries() {
5581
success: true
5682
exit_code: 0
5783
----- stdout -----
58-
Removed 1 repo, 1 hook env ([SIZE])
84+
Removed 1 repo, 1 hook env, 1 cache entry ([SIZE])
5985
6086
Removed 1 repo:
6187
- https://github.com/pre-commit/pre-commit-hooks@v1.0.0
6288
path: [HOME]/repos/deadbeef
6389
6490
Removed 1 hook env:
65-
- hook-env-dead
91+
- python env
6692
path: [HOME]/hooks/hook-env-dead
93+
language: python (3.12.0)
94+
repo: https://example.com/repo@v1.0.0
95+
deps: dep1, dep2, dep3, dep4, dep5, dep6, … (+1 more)
96+
97+
Removed 1 cache entry:
98+
- go
99+
path: [HOME]/cache/go
67100
68101
----- stderr -----
69102
");
@@ -220,10 +253,30 @@ fn cache_gc_prunes_unused_tool_versions() -> anyhow::Result<()> {
220253
repos:
221254
- repo: local
222255
hooks:
223-
- id: local-python
224-
name: Local Python Hook
225-
entry: "python -c \"print(1)\""
226-
language: python
256+
- id: local-python
257+
name: Local Python Hook
258+
entry: "python -c \"print(1)\""
259+
language: python
260+
- id: local-pygrep
261+
name: Local Pygrep Hook
262+
entry: "python -c \"print(1)\""
263+
language: pygrep
264+
- id: local-node
265+
name: Local Node Hook
266+
entry: "node -e \"console.log(1)\""
267+
language: node
268+
- id: local-go
269+
name: Local Go Hook
270+
entry: "go version"
271+
language: golang
272+
- id: local-ruby
273+
name: Local Ruby Hook
274+
entry: "ruby -e 'puts 1'"
275+
language: ruby
276+
- id: local-rust
277+
name: Local Rust Hook
278+
entry: "rustc --version"
279+
language: rust
227280
"#});
228281

229282
let home = context.home_dir();
@@ -232,36 +285,88 @@ fn cache_gc_prunes_unused_tool_versions() -> anyhow::Result<()> {
232285
let config_path = context.work_dir().child(CONFIG_FILE);
233286
write_config_tracking_file(home, &[config_path.path()])?;
234287

235-
// Seed a "used" python hook env marker so GC can read `.prek-hook.json` and retain the
236-
// corresponding Python tool version.
237-
let env_dir = home.child("hooks/python-keep");
238-
env_dir.create_dir_all()?;
239-
240-
let py_312 = home.child("tools/python/3.12.0");
241-
let py_311 = home.child("tools/python/3.11.0");
242-
py_312.create_dir_all()?;
243-
py_311.create_dir_all()?;
288+
// Seed "used" hook env markers so GC can read `.prek-hook.json` and retain the
289+
// corresponding tool versions per language.
290+
let env_py = home.child("hooks/python-keep");
291+
let env_node = home.child("hooks/node-keep");
292+
let env_go = home.child("hooks/go-keep");
293+
let env_ruby = home.child("hooks/ruby-remove");
294+
let env_rust = home.child("hooks/rust-remove");
295+
env_py.create_dir_all()?;
296+
env_node.create_dir_all()?;
297+
env_go.create_dir_all()?;
298+
env_ruby.create_dir_all()?;
299+
env_rust.create_dir_all()?;
300+
301+
let py_keep = home.child("tools/python/3.12.0");
302+
let py_remove = home.child("tools/python/3.11.0");
303+
py_keep.create_dir_all()?;
304+
py_remove.create_dir_all()?;
305+
306+
let node_keep = home.child("tools/node/22.0.0");
307+
let node_remove = home.child("tools/node/21.0.0");
308+
node_keep.create_dir_all()?;
309+
node_remove.create_dir_all()?;
310+
311+
let go_keep = home.child("tools/go/1.24.0");
312+
let go_remove = home.child("tools/go/1.23.0");
313+
go_keep.create_dir_all()?;
314+
go_remove.create_dir_all()?;
244315

245316
// Match logic for local hooks: empty deps + language request is `Any` by default.
246-
let marker = json!({
317+
let marker_py = json!({
247318
"language": "python",
248319
"language_version": "3.12.0",
249320
"dependencies": [],
250-
"env_path": env_dir.path(),
251-
"toolchain": py_312.child("bin/python").path(),
321+
"env_path": env_py.path(),
322+
"toolchain": py_keep.child("bin/python").path(),
252323
"extra": {},
253324
});
254-
env_dir
325+
env_py
255326
.child(".prek-hook.json")
256-
.write_str(&serde_json::to_string_pretty(&marker)?)?;
327+
.write_str(&serde_json::to_string_pretty(&marker_py)?)?;
328+
329+
let marker_node = json!({
330+
"language": "node",
331+
"language_version": "22.0.0",
332+
"dependencies": [],
333+
"env_path": env_node.path(),
334+
"toolchain": node_keep.child("bin/node").path(),
335+
"extra": {},
336+
});
337+
env_node
338+
.child(".prek-hook.json")
339+
.write_str(&serde_json::to_string_pretty(&marker_node)?)?;
340+
341+
let marker_go = json!({
342+
"language": "golang",
343+
"language_version": "1.24.0",
344+
"dependencies": [],
345+
"env_path": env_go.path(),
346+
"toolchain": go_keep.child("bin/go").path(),
347+
"extra": {},
348+
});
349+
env_go
350+
.child(".prek-hook.json")
351+
.write_str(&serde_json::to_string_pretty(&marker_go)?)?;
257352

258353
cmd_snapshot!(context.filters(), context.command().args(["cache", "gc", "--dry-run", "-v"]), @r"
259354
success: true
260355
exit_code: 0
261356
----- stdout -----
262-
Would remove 1 tool ([SIZE])
263-
264-
Would remove 1 tool:
357+
Would remove 2 hook envs, 3 tools ([SIZE])
358+
359+
Would remove 2 hook envs:
360+
- ruby-remove
361+
path: [HOME]/hooks/ruby-remove
362+
- rust-remove
363+
path: [HOME]/hooks/rust-remove
364+
365+
Would remove 3 tools:
366+
- go/1.23.0
367+
path: [HOME]/tools/go/1.23.0
368+
- node/21.0.0
369+
path: [HOME]/tools/node/21.0.0
265370
- python/3.11.0
266371
path: [HOME]/tools/python/3.11.0
267372

0 commit comments

Comments
 (0)