use std::path::Path;
use super::plan::{OverwritePolicy, Plan, PlanError};
use crate::cli::StubsCommand;
use crate::error::CommandError;
use crate::project;
struct Stub {
path: std::path::PathBuf,
content: String,
}
pub(crate) fn build_plan(root: &Path) -> Plan {
let stubs = stubs();
let mut plan = Plan::new();
for stub in stubs {
let path = resolve(root, &stub.path);
plan = plan.create(path, stub.content);
}
plan
}
fn stubs() -> Vec<Stub> {
vec![Stub {
path: std::path::PathBuf::from("tests/helpers/mod.rs"),
content: helpers_mod_rs(),
}]
}
fn helpers_mod_rs() -> String {
"\
//! Shared helpers for the application's integration tests.
//!
//! Each integration test in `tests/` is its own crate. Include this module
//! from a test file with `mod helpers;` and add shared setup, fixtures, or
//! assertions here so individual test files stay focused on behaviour.
//!
//! Generated by `arc stubs publish`. Edit freely; `arc stubs publish` will
//! refuse to overwrite an existing `tests/helpers/mod.rs` unless you pass
//! `--force`.
use std::path::PathBuf;
/// The project root directory (the package's `CARGO_MANIFEST_DIR`).
///
/// Resolved at compile time from the test crate's environment, so it is the
/// directory containing the `Cargo.toml` of the package under test — the
/// project root. Useful for locating fixtures, migration SQL, or assets
/// relative to the repository rather than the test's working directory.
#[must_use]
pub fn project_root() -> PathBuf {
PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"))
}
"
.to_owned()
}
fn resolve(root: &Path, relative: &Path) -> std::path::PathBuf {
root.join(relative)
}
pub(crate) fn execute_stubs(cmd: StubsCommand) -> Result<(), CommandError> {
let project = project::discover()?;
let root = project.root().to_path_buf();
match cmd {
StubsCommand::Publish { dry_run, force } => {
publish(&root, dry_run, force).map_err(CommandError::Metadata)
}
}
}
fn publish(root: &Path, dry_run: bool, force: bool) -> Result<(), String> {
let overwrite = if force {
OverwritePolicy::Overwrite
} else {
OverwritePolicy::Refuse
};
let mut plan = build_plan(root);
if plan.is_empty() {
println!("no stubs to publish");
return Ok(());
}
if dry_run {
let report = plan.dry_run_report(overwrite).map_err(|e| e.to_string())?;
println!("{report}");
return Ok(());
}
match plan.execute(overwrite, None) {
Ok(written) => {
for path in &written {
println!("created {}", path.display());
}
println!("stubs published: {}", written.len());
Ok(())
}
Err(PlanError::Conflict(path)) => Err(format!(
"refusing to overwrite existing file: {}; pass --force to overwrite",
path.display()
)),
Err(error) => Err(error.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn temp_root(label: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"arcature-cli-stubs-{label}-{}-{}",
std::process::id(),
unique_suffix()
));
fs::create_dir_all(&root).expect("temp root should be created");
root
}
fn unique_suffix() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos())
}
fn cleanup(root: &Path) {
let _ = fs::remove_dir_all(root);
}
#[test]
fn helpers_mod_rs_is_real_and_compilable() {
let content = helpers_mod_rs();
assert!(content.contains("pub fn project_root()"));
assert!(content.contains("use std::path::PathBuf;"));
assert!(content.contains("CARGO_MANIFEST_DIR"));
assert!(!content.contains("TODO"));
assert!(!content.contains("todo!"));
assert!(!content.contains("unimplemented!"));
assert!(!content.contains("unwrap()"));
}
#[test]
fn build_plan_targets_tests_helpers_mod_rs() {
let root = temp_root("plan");
let plan = build_plan(&root);
let ops = plan.ops();
assert_eq!(ops.len(), 1);
match &ops[0] {
super::super::plan::PlannedOp::Create { path, .. } => {
assert_eq!(*path, root.join("tests").join("helpers").join("mod.rs"));
}
_ => panic!("expected a Create op"),
}
cleanup(&root);
}
#[test]
fn publish_creates_helpers_under_project_root() {
let root = temp_root("publish");
publish(&root, false, false).expect("publish should succeed");
let file = root.join("tests").join("helpers").join("mod.rs");
assert!(file.is_file(), "tests/helpers/mod.rs should be created");
let content = fs::read_to_string(&file).unwrap();
assert!(content.contains("pub fn project_root()"));
cleanup(&root);
}
#[test]
fn dry_run_publishes_nothing() {
let root = temp_root("dry-run");
publish(&root, true, false).expect("dry-run should succeed");
assert!(
!root.join("tests").exists(),
"dry run must not create directories"
);
cleanup(&root);
}
#[test]
fn publish_refuses_overwrite_without_force() {
let root = temp_root("refuse");
let file = root.join("tests").join("helpers").join("mod.rs");
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, "user helpers").unwrap();
let err = publish(&root, false, false).expect_err("should refuse to overwrite");
assert!(err.contains("refusing to overwrite"));
assert!(err.contains("--force"));
assert_eq!(fs::read_to_string(&file).unwrap(), "user helpers");
cleanup(&root);
}
#[test]
fn publish_with_force_overwrites_existing() {
let root = temp_root("force");
let file = root.join("tests").join("helpers").join("mod.rs");
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, "user helpers").unwrap();
publish(&root, false, true).expect("force should overwrite");
let content = fs::read_to_string(&file).unwrap();
assert!(content.contains("pub fn project_root()"));
assert!(!content.contains("user helpers"));
cleanup(&root);
}
}