ralph/commands/tutorial/
sandbox.rs1use anyhow::{Context, Result};
24use std::path::{Path, PathBuf};
25use tempfile::TempDir;
26
27use crate::runutil::{ManagedCommand, TimeoutClass, execute_checked_command};
28
29const SAMPLE_CARGO_TOML: &str = r#"[package]
31name = "tutorial-project"
32version = "0.1.0"
33edition = "2021"
34
35[dependencies]
36"#;
37
38const SAMPLE_LIB_RS: &str = r#"//! Tutorial project for Ralph onboarding.
39//!
40//! Add your code here.
41
42/// Returns a greeting message.
43pub fn greet(name: &str) -> String {
44 format!("Hello, {}!", name)
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn test_greet() {
53 assert_eq!(greet("World"), "Hello, World!");
54 }
55}
56"#;
57
58fn run_tutorial_git(path: &Path, description: &str, args: &[&str]) -> Result<()> {
59 let mut command = std::process::Command::new("git");
60 command.current_dir(path).args(args);
61
62 execute_checked_command(ManagedCommand::new(command, description, TimeoutClass::Git))
63 .with_context(|| format!("{description} in tutorial sandbox {}", path.display()))?;
64 Ok(())
65}
66
67pub struct TutorialSandbox {
69 temp_dir: Option<TempDir>,
71 pub path: PathBuf,
73}
74
75impl TutorialSandbox {
76 pub fn create() -> Result<Self> {
78 let temp_dir =
79 TempDir::new().context("failed to create temp directory for tutorial sandbox")?;
80 let path = temp_dir.path().to_path_buf();
81
82 run_tutorial_git(
83 &path,
84 "initialize tutorial git repository",
85 &["init", "--quiet"],
86 )?;
87 run_tutorial_git(
88 &path,
89 "configure tutorial git user.name",
90 &["config", "user.name", "Ralph Tutorial"],
91 )?;
92 run_tutorial_git(
93 &path,
94 "configure tutorial git user.email",
95 &["config", "user.email", "tutorial@ralph.invalid"],
96 )?;
97
98 std::fs::write(path.join("Cargo.toml"), SAMPLE_CARGO_TOML)?;
100 std::fs::create_dir_all(path.join("src"))?;
101 std::fs::write(path.join("src/lib.rs"), SAMPLE_LIB_RS)?;
102
103 std::fs::write(
105 path.join(".gitignore"),
106 "/target\n.ralph/lock\n.ralph/cache/\n.ralph/logs/\n",
107 )?;
108
109 run_tutorial_git(&path, "stage tutorial sandbox files", &["add", "."])?;
110 run_tutorial_git(
111 &path,
112 "create tutorial sandbox initial commit",
113 &["commit", "--quiet", "-m", "Initial commit"],
114 )?;
115
116 Ok(Self {
117 temp_dir: Some(temp_dir),
118 path,
119 })
120 }
121
122 pub fn preserve(mut self) -> PathBuf {
124 let path = self.path.clone();
125 if let Some(temp_dir) = self.temp_dir.take() {
127 let _ = temp_dir.keep();
129 }
130 path
131 }
132}
133
134impl Drop for TutorialSandbox {
135 fn drop(&mut self) {
136 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[cfg(unix)]
145 fn write_fake_git(bin_dir: &Path, script: &str) {
146 use std::os::unix::fs::PermissionsExt;
147
148 let path = bin_dir.join("git");
149 std::fs::write(&path, script).expect("write fake git");
150 let mut perms = std::fs::metadata(&path)
151 .expect("fake git metadata")
152 .permissions();
153 perms.set_mode(0o755);
154 std::fs::set_permissions(&path, perms).expect("chmod fake git");
155 }
156
157 #[test]
158 fn sandbox_creates_files() {
159 let _path_guard = crate::testsupport::path::path_lock()
160 .lock()
161 .expect("path lock");
162 let sandbox = TutorialSandbox::create().unwrap();
163
164 assert!(sandbox.path.join("Cargo.toml").exists());
165 assert!(sandbox.path.join("src/lib.rs").exists());
166 assert!(sandbox.path.join(".gitignore").exists());
167 assert!(sandbox.path.join(".git").exists());
168 }
169
170 #[cfg(unix)]
171 #[test]
172 fn sandbox_create_fails_when_git_configuration_fails() {
173 let temp = TempDir::new().unwrap();
174 let bin_dir = temp.path().join("bin");
175 std::fs::create_dir_all(&bin_dir).unwrap();
176 write_fake_git(
177 &bin_dir,
178 r#"#!/bin/sh
179if [ "$1" = "init" ]; then
180 exit 0
181fi
182if [ "$1" = "config" ] && [ "$2" = "user.name" ]; then
183 echo "fake git failing: config" >&2
184 exit 7
185fi
186exit 0
187"#,
188 );
189
190 let err =
191 match crate::testsupport::path::with_prepend_path(&bin_dir, TutorialSandbox::create) {
192 Ok(_) => panic!("sandbox creation should fail when git config fails"),
193 Err(err) => err,
194 };
195 let text = format!("{err:#}");
196 assert!(text.contains("configure tutorial git user.name"));
197 assert!(text.contains("fake git failing: config"));
198 }
199
200 #[test]
201 fn sandbox_preserve_prevents_cleanup() {
202 let _path_guard = crate::testsupport::path::path_lock()
203 .lock()
204 .expect("path lock");
205 let sandbox = TutorialSandbox::create().unwrap();
206 let path = sandbox.preserve();
207
208 assert!(path.exists());
210
211 let _ = std::fs::remove_dir_all(&path);
213 }
214}