use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use crate::runutil::{ManagedCommand, TimeoutClass, execute_checked_command};
const SAMPLE_CARGO_TOML: &str = r#"[package]
name = "tutorial-project"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
const SAMPLE_LIB_RS: &str = r#"//! Tutorial project for Ralph onboarding.
//!
//! Add your code here.
/// Returns a greeting message.
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
assert_eq!(greet("World"), "Hello, World!");
}
}
"#;
fn run_tutorial_git(path: &Path, description: &str, args: &[&str]) -> Result<()> {
let mut command = std::process::Command::new("git");
command.current_dir(path).args(args);
execute_checked_command(ManagedCommand::new(command, description, TimeoutClass::Git))
.with_context(|| format!("{description} in tutorial sandbox {}", path.display()))?;
Ok(())
}
pub struct TutorialSandbox {
temp_dir: Option<TempDir>,
pub path: PathBuf,
}
impl TutorialSandbox {
pub fn create() -> Result<Self> {
let temp_dir =
TempDir::new().context("failed to create temp directory for tutorial sandbox")?;
let path = temp_dir.path().to_path_buf();
run_tutorial_git(
&path,
"initialize tutorial git repository",
&["init", "--quiet"],
)?;
run_tutorial_git(
&path,
"configure tutorial git user.name",
&["config", "user.name", "Ralph Tutorial"],
)?;
run_tutorial_git(
&path,
"configure tutorial git user.email",
&["config", "user.email", "tutorial@ralph.invalid"],
)?;
std::fs::write(path.join("Cargo.toml"), SAMPLE_CARGO_TOML)?;
std::fs::create_dir_all(path.join("src"))?;
std::fs::write(path.join("src/lib.rs"), SAMPLE_LIB_RS)?;
std::fs::write(
path.join(".gitignore"),
"/target\n.ralph/lock\n.ralph/cache/\n.ralph/logs/\n",
)?;
run_tutorial_git(&path, "stage tutorial sandbox files", &["add", "."])?;
run_tutorial_git(
&path,
"create tutorial sandbox initial commit",
&["commit", "--quiet", "-m", "Initial commit"],
)?;
Ok(Self {
temp_dir: Some(temp_dir),
path,
})
}
pub fn preserve(mut self) -> PathBuf {
let path = self.path.clone();
if let Some(temp_dir) = self.temp_dir.take() {
let _ = temp_dir.keep();
}
path
}
}
impl Drop for TutorialSandbox {
fn drop(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn write_fake_git(bin_dir: &Path, script: &str) {
use std::os::unix::fs::PermissionsExt;
let path = bin_dir.join("git");
std::fs::write(&path, script).expect("write fake git");
let mut perms = std::fs::metadata(&path)
.expect("fake git metadata")
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).expect("chmod fake git");
}
#[test]
fn sandbox_creates_files() {
let _path_guard = crate::testsupport::path::path_lock()
.lock()
.expect("path lock");
let sandbox = TutorialSandbox::create().unwrap();
assert!(sandbox.path.join("Cargo.toml").exists());
assert!(sandbox.path.join("src/lib.rs").exists());
assert!(sandbox.path.join(".gitignore").exists());
assert!(sandbox.path.join(".git").exists());
}
#[cfg(unix)]
#[test]
fn sandbox_create_fails_when_git_configuration_fails() {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
write_fake_git(
&bin_dir,
r#"#!/bin/sh
if [ "$1" = "init" ]; then
exit 0
fi
if [ "$1" = "config" ] && [ "$2" = "user.name" ]; then
echo "fake git failing: config" >&2
exit 7
fi
exit 0
"#,
);
let err =
match crate::testsupport::path::with_prepend_path(&bin_dir, TutorialSandbox::create) {
Ok(_) => panic!("sandbox creation should fail when git config fails"),
Err(err) => err,
};
let text = format!("{err:#}");
assert!(text.contains("configure tutorial git user.name"));
assert!(text.contains("fake git failing: config"));
}
#[test]
fn sandbox_preserve_prevents_cleanup() {
let _path_guard = crate::testsupport::path::path_lock()
.lock()
.expect("path lock");
let sandbox = TutorialSandbox::create().unwrap();
let path = sandbox.preserve();
assert!(path.exists());
let _ = std::fs::remove_dir_all(&path);
}
}