#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::needless_raw_string_hashes,
clippy::duration_suboptimal_units,
clippy::branches_sharing_code,
clippy::used_underscore_binding,
clippy::single_char_pattern,
clippy::ignore_without_reason,
clippy::cloned_ref_to_slice_refs,
clippy::doc_overindented_list_items,
clippy::match_wildcard_for_single_variants,
clippy::ignored_unit_patterns,
clippy::needless_collect,
clippy::unnecessary_map_or,
clippy::manual_flatten,
clippy::manual_strip,
clippy::future_not_send,
clippy::unnested_or_patterns,
clippy::no_effect_underscore_binding,
clippy::literal_string_with_formatting_args
)]
#![cfg(any())]
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
fn create_test_project() -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let make_toml = r#"
[project]
name = "test-project"
type = "webapp"
version = "1.0.0"
description = "Test project for lifecycle e2e tests"
[lifecycle.validate]
description = "Validate environment"
commands = ["echo '🔍 Validating environment...'"]
[lifecycle.lint]
description = "Lint code"
commands = ["echo '🔍 Linting code...'"]
[lifecycle.init]
description = "Initialize project structure"
commands = [
"mkdir -p src tests docs",
"echo 'Project initialized' > .initialized"
]
[lifecycle.setup]
description = "Install dependencies and configure environment"
commands = [
"echo 'Installing dependencies...'",
"echo 'Setup complete' > .setup"
]
watch = true
[lifecycle.dev]
description = "Start development server"
commands = ["echo 'Dev server running on port 3000'"]
port = 3000
watch = true
[lifecycle.build]
description = "Build production artifacts"
commands = [
"echo 'Building project...'",
"mkdir -p dist",
"echo 'Build complete' > dist/output.txt"
]
cache = true
[lifecycle.test]
description = "Run test suite"
commands = [
"echo 'Running tests...'",
"echo 'All tests passed' > .test-results"
]
[lifecycle.deploy]
description = "Deploy to production"
commands = ["echo 'Deploying to production...'"]
[lifecycle.cleanup]
description = "Clean up temporary files"
commands = ["echo '🧹 Cleaning up...'"]
[hooks]
before_build = ["lint"]
after_build = ["test"]
"#;
fs::write(temp_dir.path().join("make.toml"), make_toml).expect("Failed to write make.toml");
temp_dir
}
fn ggen_cmd() -> Command {
Command::cargo_bin("ggen").expect("Failed to find ggen binary")
}
fn assert_state_exists(root: &std::path::Path) {
let state_path = root.join(".ggen/state.json");
assert!(
state_path.exists(),
"State file should exist at {}",
state_path.display()
);
let state_content = fs::read_to_string(&state_path).expect("Failed to read state.json");
serde_json::from_str::<serde_json::Value>(&state_content)
.expect("State file should be valid JSON");
}
#[test]
fn test_lifecycle_list_shows_all_phases() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("list")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Available lifecycle phases"))
.stdout(predicate::str::contains("init"))
.stdout(predicate::str::contains("setup"))
.stdout(predicate::str::contains("dev"))
.stdout(predicate::str::contains("build"))
.stdout(predicate::str::contains("test"))
.stdout(predicate::str::contains("deploy"))
.stdout(predicate::str::contains("Initialize project structure"));
}
#[test]
fn test_lifecycle_list_without_make_toml() {
let temp_dir = TempDir::new().unwrap();
ggen_cmd()
.arg("lifecycle")
.arg("list")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("No make.toml found"));
}
#[test]
fn test_lifecycle_show_displays_phase_details() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("show")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Phase: init"))
.stdout(predicate::str::contains("Initialize project structure"))
.stdout(predicate::str::contains("Commands:"))
.stdout(predicate::str::contains("mkdir -p src tests docs"));
}
#[test]
fn test_lifecycle_show_with_metadata() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("show")
.arg("dev")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Watch mode: true"))
.stdout(predicate::str::contains("Port: 3000"));
ggen_cmd()
.arg("lifecycle")
.arg("show")
.arg("build")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Caching: true"))
.stdout(predicate::str::contains("Before hooks:"))
.stdout(predicate::str::contains("lint"))
.stdout(predicate::str::contains("After hooks:"))
.stdout(predicate::str::contains("test"));
}
#[test]
fn test_lifecycle_show_missing_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("show")
.arg("nonexistent")
.arg("--root")
.arg(temp_dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("Phase 'nonexistent' not found"));
}
#[test]
fn test_lifecycle_run_executes_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Running phase: init"));
assert!(temp_dir.path().join("src").exists());
assert!(temp_dir.path().join("tests").exists());
assert!(temp_dir.path().join("docs").exists());
assert!(temp_dir.path().join(".initialized").exists());
}
#[test]
fn test_lifecycle_run_creates_state_file() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
assert_state_exists(temp_dir.path());
let state_path = temp_dir.path().join(".ggen/state.json");
let state_content = fs::read_to_string(&state_path).unwrap();
let state: serde_json::Value = serde_json::from_str(&state_content).unwrap();
assert_eq!(state["last_phase"], "init");
assert!(state["phase_history"]
.as_array()
.unwrap()
.iter()
.any(|record| record["phase"] == "init"));
}
#[test]
fn test_lifecycle_run_with_environment() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("setup")
.arg("--root")
.arg(temp_dir.path())
.arg("--env")
.arg("production")
.assert()
.success()
.stdout(predicate::str::contains("Running phase: setup"));
assert_state_exists(temp_dir.path());
}
#[test]
fn test_lifecycle_run_missing_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("invalid-phase")
.arg("--root")
.arg(temp_dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("Phase 'invalid-phase' not found"));
}
#[test]
fn test_lifecycle_pipeline_sequential_execution() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("pipeline")
.arg("init")
.arg("setup")
.arg("build")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Running phase: init"))
.stdout(predicate::str::contains("Running phase: setup"))
.stdout(predicate::str::contains("Running phase: build"))
.stdout(predicate::str::contains(
"Pipeline completed: init → setup → build",
));
assert!(temp_dir.path().join(".initialized").exists());
assert!(temp_dir.path().join(".setup").exists());
assert!(temp_dir.path().join("dist/output.txt").exists());
let state_path = temp_dir.path().join(".ggen/state.json");
let state_content = fs::read_to_string(&state_path).unwrap();
let state: serde_json::Value = serde_json::from_str(&state_content).unwrap();
assert_eq!(state["last_phase"], "test");
let phase_history = state["phase_history"].as_array().unwrap();
assert!(phase_history.iter().any(|record| record["phase"] == "init"));
assert!(phase_history
.iter()
.any(|record| record["phase"] == "setup"));
assert!(phase_history
.iter()
.any(|record| record["phase"] == "build"));
}
#[test]
fn test_lifecycle_pipeline_with_environment() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("pipeline")
.arg("init")
.arg("setup")
.arg("--root")
.arg(temp_dir.path())
.arg("--env")
.arg("staging")
.assert()
.success();
assert_state_exists(temp_dir.path());
}
#[test]
fn test_lifecycle_pipeline_stops_on_missing_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("pipeline")
.arg("init")
.arg("nonexistent")
.arg("setup")
.arg("--root")
.arg(temp_dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("Phase 'nonexistent' not found"));
}
#[test]
fn test_state_persistence_across_runs() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("setup")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
let state_path = temp_dir.path().join(".ggen/state.json");
let state_content = fs::read_to_string(&state_path).unwrap();
let state: serde_json::Value = serde_json::from_str(&state_content).unwrap();
assert_eq!(state["last_phase"], "setup");
let phase_history = state["phase_history"].as_array().unwrap();
assert_eq!(phase_history.len(), 2);
}
#[test]
fn test_lifecycle_list_shows_last_executed_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("build")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
ggen_cmd()
.arg("lifecycle")
.arg("list")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Last executed: test"));
}
#[test]
fn test_lifecycle_help_output() {
ggen_cmd()
.arg("lifecycle")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Universal lifecycle management"))
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("show"))
.stdout(predicate::str::contains("run"))
.stdout(predicate::str::contains("pipeline"));
}
#[test]
fn test_lifecycle_list_help() {
ggen_cmd()
.arg("lifecycle")
.arg("list")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"List all available lifecycle phases",
))
.stdout(predicate::str::contains("--root"));
}
#[test]
fn test_lifecycle_show_help() {
ggen_cmd()
.arg("lifecycle")
.arg("show")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Show details of a specific phase"))
.stdout(predicate::str::contains("<PHASE>"))
.stdout(predicate::str::contains("--root"));
}
#[test]
fn test_lifecycle_run_help() {
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Run a single lifecycle phase"))
.stdout(predicate::str::contains("<PHASE>"))
.stdout(predicate::str::contains("--root"))
.stdout(predicate::str::contains("--env"));
}
#[test]
fn test_lifecycle_pipeline_help() {
ggen_cmd()
.arg("lifecycle")
.arg("pipeline")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Run multiple phases in sequence"))
.stdout(predicate::str::contains("[PHASES]..."))
.stdout(predicate::str::contains("--root"))
.stdout(predicate::str::contains("--env"));
}
#[test]
fn test_hooks_execution_order() {
let temp_dir = create_test_project();
let output = ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("build")
.arg("--root")
.arg(temp_dir.path())
.output()
.expect("Failed to execute command");
let stdout = String::from_utf8_lossy(&output.stdout);
let lint_pos = stdout.find("Running phase: lint").unwrap();
let build_pos = stdout.find("Running phase: build").unwrap();
let test_pos = stdout.find("Running phase: test").unwrap();
assert!(lint_pos < build_pos, "Lint should run before build");
assert!(build_pos < test_pos, "Test should run after build");
}
#[test]
fn test_multiple_commands_in_phase() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("build")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Building project..."));
assert!(temp_dir.path().join("dist").exists());
assert!(temp_dir.path().join("dist/output.txt").exists());
}
#[test]
fn test_empty_phase_list() {
let temp_dir = TempDir::new().unwrap();
let make_toml = r#"
[project]
name = "test-project"
"#;
fs::write(temp_dir.path().join("make.toml"), make_toml).unwrap();
ggen_cmd()
.arg("lifecycle")
.arg("list")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("(no phases defined)"));
}
#[test]
fn test_state_directory_creation() {
let temp_dir = create_test_project();
assert!(!temp_dir.path().join(".ggen").exists());
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
assert!(temp_dir.path().join(".ggen").exists());
assert!(temp_dir.path().join(".ggen").is_dir());
assert_state_exists(temp_dir.path());
}
#[test]
fn test_concurrent_safe_state_updates() {
let temp_dir = create_test_project();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("setup")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("test")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
let state_path = temp_dir.path().join(".ggen/state.json");
let state_content = fs::read_to_string(&state_path).unwrap();
let state: serde_json::Value = serde_json::from_str(&state_content).unwrap();
let phase_history = state["phase_history"].as_array().unwrap();
assert_eq!(phase_history.len(), 3);
}
#[test]
fn test_phase_without_hooks() {
let temp_dir = create_test_project();
let output = ggen_cmd()
.arg("lifecycle")
.arg("run")
.arg("init")
.arg("--root")
.arg(temp_dir.path())
.output()
.expect("Failed to execute command");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Running phase: init"));
assert!(!stdout.contains("Running phase: lint"));
assert!(!stdout.contains("Running phase: test"));
}
#[test]
fn test_performance_fast_execution() {
let temp_dir = create_test_project();
let start = std::time::Instant::now();
ggen_cmd()
.arg("lifecycle")
.arg("pipeline")
.arg("init")
.arg("setup")
.arg("build")
.arg("test")
.arg("--root")
.arg(temp_dir.path())
.assert()
.success();
let duration = start.elapsed();
assert!(
duration.as_secs() < 5,
"Pipeline took too long: {:?}",
duration
);
}