lean_ctx/core/eval_ab/testbench/
clone.rs1use std::path::{Path, PathBuf};
14use std::process::Command;
15
16use anyhow::{Context, Result, bail};
17
18use super::lockfile::RepoEntry;
19
20pub fn materialize(repo: &RepoEntry, lock_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
22 if let Some(rel) = &repo.path {
23 let dir = lock_dir.join(rel);
24 if !dir.is_dir() {
25 bail!(
26 "repo {}: local fixture {} is not a directory",
27 repo.name,
28 dir.display()
29 );
30 }
31 return Ok(dir);
32 }
33
34 let url = repo
36 .url
37 .as_deref()
38 .context("remote repo entry without url (should be unreachable)")?;
39 let commit = repo
40 .commit
41 .as_deref()
42 .context("remote repo entry without commit (should be unreachable)")?;
43
44 std::fs::create_dir_all(cache_dir)
45 .with_context(|| format!("creating cache dir {}", cache_dir.display()))?;
46 let dest = cache_dir.join(&repo.name);
47
48 if !dest.join(".git").is_dir() {
49 git(&["clone", "--quiet", url, &dest.to_string_lossy()], None)
53 .with_context(|| format!("cloning {url} for repo {}", repo.name))?;
54 }
55
56 if git(&["checkout", "--quiet", commit], Some(&dest)).is_err() {
59 git(&["fetch", "--quiet", "--all", "--tags"], Some(&dest))
60 .with_context(|| format!("fetching {url} for repo {}", repo.name))?;
61 git(&["checkout", "--quiet", commit], Some(&dest)).with_context(|| {
62 format!("checking out pinned commit {commit} in repo {}", repo.name)
63 })?;
64 }
65
66 let head = git(&["rev-parse", "HEAD"], Some(&dest))?;
67 let head = head.trim();
68 if head != commit && !head.starts_with(commit) {
69 bail!(
70 "repo {}: checked-out HEAD {head} does not match pinned commit {commit}",
71 repo.name
72 );
73 }
74 Ok(dest)
75}
76
77fn git(args: &[&str], dir: Option<&Path>) -> Result<String> {
80 let mut cmd = Command::new("git");
81 if let Some(d) = dir {
82 cmd.current_dir(d);
83 }
84 cmd.args(args);
85 let out = cmd
86 .output()
87 .with_context(|| format!("spawning git {}", args.join(" ")))?;
88 if !out.status.success() {
89 let stderr = String::from_utf8_lossy(&out.stderr);
90 bail!("git {} failed: {}", args.join(" "), stderr.trim());
91 }
92 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn local_fixture_resolves_against_lock_dir() {
101 let root = tempfile::tempdir().unwrap();
102 std::fs::create_dir_all(root.path().join("repos/qa")).unwrap();
103 let entry = RepoEntry {
104 name: "qa".into(),
105 url: None,
106 commit: None,
107 path: Some("repos/qa".into()),
108 suite: "qa.ndjson".into(),
109 };
110 let got = materialize(&entry, root.path(), &root.path().join("cache")).unwrap();
111 assert_eq!(got, root.path().join("repos/qa"));
112 }
113
114 #[test]
115 fn missing_local_fixture_errors() {
116 let root = tempfile::tempdir().unwrap();
117 let entry = RepoEntry {
118 name: "qa".into(),
119 url: None,
120 commit: None,
121 path: Some("nope".into()),
122 suite: "qa.ndjson".into(),
123 };
124 assert!(materialize(&entry, root.path(), &root.path().join("cache")).is_err());
125 }
126
127 #[cfg(unix)]
128 #[test]
129 fn clones_and_checks_out_a_local_git_repo() {
130 let root = tempfile::tempdir().unwrap();
133 let origin = root.path().join("origin");
134 std::fs::create_dir_all(&origin).unwrap();
135 let run = |args: &[&str]| {
136 assert!(
137 Command::new("git")
138 .current_dir(&origin)
139 .args(args)
140 .output()
141 .unwrap()
142 .status
143 .success(),
144 "git {args:?} failed"
145 );
146 };
147 run(&["init", "--quiet"]);
148 run(&["config", "user.email", "t@t"]);
149 run(&["config", "user.name", "t"]);
150 std::fs::write(origin.join("file.txt"), "hello").unwrap();
151 run(&["add", "."]);
152 run(&["commit", "--quiet", "-m", "init"]);
153 let commit = String::from_utf8_lossy(
154 &Command::new("git")
155 .current_dir(&origin)
156 .args(["rev-parse", "HEAD"])
157 .output()
158 .unwrap()
159 .stdout,
160 )
161 .trim()
162 .to_string();
163
164 let entry = RepoEntry {
165 name: "fix".into(),
166 url: Some(origin.to_string_lossy().into_owned()),
167 commit: Some(commit),
168 path: None,
169 suite: "s.ndjson".into(),
170 };
171 let cache = root.path().join("cache");
172 let dest = materialize(&entry, root.path(), &cache).unwrap();
173 assert!(dest.join("file.txt").exists());
174 let dest2 = materialize(&entry, root.path(), &cache).unwrap();
176 assert_eq!(dest, dest2);
177 }
178
179 #[cfg(unix)]
180 #[test]
181 fn wrong_pinned_commit_errors() {
182 let root = tempfile::tempdir().unwrap();
183 let origin = root.path().join("origin");
184 std::fs::create_dir_all(&origin).unwrap();
185 for args in [
186 vec!["init", "--quiet"],
187 vec!["config", "user.email", "t@t"],
188 vec!["config", "user.name", "t"],
189 ] {
190 Command::new("git")
191 .current_dir(&origin)
192 .args(&args)
193 .output()
194 .unwrap();
195 }
196 std::fs::write(origin.join("f"), "x").unwrap();
197 for args in [vec!["add", "."], vec!["commit", "--quiet", "-m", "i"]] {
198 Command::new("git")
199 .current_dir(&origin)
200 .args(&args)
201 .output()
202 .unwrap();
203 }
204 let entry = RepoEntry {
205 name: "fix".into(),
206 url: Some(origin.to_string_lossy().into_owned()),
207 commit: Some("0000000000000000000000000000000000000000".into()),
208 path: None,
209 suite: "s.ndjson".into(),
210 };
211 assert!(materialize(&entry, root.path(), &root.path().join("cache")).is_err());
212 }
213}