Skip to main content

ralph/commands/tutorial/
sandbox.rs

1//! Sandbox creation utilities for tutorial.
2//!
3//! Responsibilities:
4//! - Create temporary directory outside repo.
5//! - Initialize git repository with sample files.
6//! - Provide cleanup handling with --keep-sandbox support.
7
8use anyhow::{Context, Result};
9use std::path::PathBuf;
10use tempfile::TempDir;
11
12/// Sample Rust project files for tutorial sandbox.
13const SAMPLE_CARGO_TOML: &str = r#"[package]
14name = "tutorial-project"
15version = "0.1.0"
16edition = "2021"
17
18[dependencies]
19"#;
20
21const SAMPLE_LIB_RS: &str = r#"//! Tutorial project for Ralph onboarding.
22//!
23//! Add your code here.
24
25/// Returns a greeting message.
26pub fn greet(name: &str) -> String {
27    format!("Hello, {}!", name)
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn test_greet() {
36        assert_eq!(greet("World"), "Hello, World!");
37    }
38}
39"#;
40
41/// A tutorial sandbox with automatic or manual cleanup.
42pub struct TutorialSandbox {
43    /// The temp directory (None if preserved).
44    temp_dir: Option<TempDir>,
45    /// The sandbox path (always available).
46    pub path: PathBuf,
47}
48
49impl TutorialSandbox {
50    /// Create a new tutorial sandbox with git init and sample files.
51    pub fn create() -> Result<Self> {
52        let temp_dir =
53            TempDir::new().context("failed to create temp directory for tutorial sandbox")?;
54        let path = temp_dir.path().to_path_buf();
55
56        // Initialize git repo
57        let status = std::process::Command::new("git")
58            .current_dir(&path)
59            .args(["init", "--quiet"])
60            .status()
61            .context("run git init")?;
62        anyhow::ensure!(status.success(), "git init failed");
63
64        // Configure git user
65        std::process::Command::new("git")
66            .current_dir(&path)
67            .args(["config", "user.name", "Ralph Tutorial"])
68            .status()
69            .context("set git user.name")?;
70        std::process::Command::new("git")
71            .current_dir(&path)
72            .args(["config", "user.email", "tutorial@ralph.invalid"])
73            .status()
74            .context("set git user.email")?;
75
76        // Create sample project files
77        std::fs::write(path.join("Cargo.toml"), SAMPLE_CARGO_TOML)?;
78        std::fs::create_dir_all(path.join("src"))?;
79        std::fs::write(path.join("src/lib.rs"), SAMPLE_LIB_RS)?;
80
81        // Create .gitignore
82        std::fs::write(
83            path.join(".gitignore"),
84            "/target\n.ralph/lock\n.ralph/cache/\n.ralph/logs/\n",
85        )?;
86
87        // Initial commit
88        std::process::Command::new("git")
89            .current_dir(&path)
90            .args(["add", "."])
91            .status()
92            .context("git add")?;
93        std::process::Command::new("git")
94            .current_dir(&path)
95            .args(["commit", "--quiet", "-m", "Initial commit"])
96            .status()
97            .context("git commit")?;
98
99        Ok(Self {
100            temp_dir: Some(temp_dir),
101            path,
102        })
103    }
104
105    /// Keep the sandbox directory (don't delete on drop).
106    pub fn preserve(mut self) -> PathBuf {
107        let path = self.path.clone();
108        // Take the temp_dir out and keep it to prevent cleanup
109        if let Some(temp_dir) = self.temp_dir.take() {
110            // Keep the directory (ignoring the Result)
111            let _ = temp_dir.keep();
112        }
113        path
114    }
115}
116
117impl Drop for TutorialSandbox {
118    fn drop(&mut self) {
119        // temp_dir auto-cleans when dropped (if not None)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn sandbox_creates_files() {
129        let sandbox = TutorialSandbox::create().unwrap();
130
131        assert!(sandbox.path.join("Cargo.toml").exists());
132        assert!(sandbox.path.join("src/lib.rs").exists());
133        assert!(sandbox.path.join(".gitignore").exists());
134        assert!(sandbox.path.join(".git").exists());
135    }
136
137    #[test]
138    fn sandbox_preserve_prevents_cleanup() {
139        let sandbox = TutorialSandbox::create().unwrap();
140        let path = sandbox.preserve();
141
142        // Path should still exist after preserve
143        assert!(path.exists());
144
145        // Clean up manually
146        let _ = std::fs::remove_dir_all(&path);
147    }
148}