#[cfg(test)]
mod integration_tests {
use super::super::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tempfile::TempDir;
struct LifecycleTestFixture {
temp_dir: TempDir,
state_path: PathBuf,
}
impl LifecycleTestFixture {
fn new() -> Self {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let state_path = temp_dir.path().join(".ggen/state.json");
let make_toml = r#"
[project]
name = "test-project"
type = "app"
version = "1.0.0"
description = "Test project for lifecycle integration"
[lifecycle.init]
description = "Initialize project"
commands = [
"mkdir -p src",
"echo 'test' > src/test.txt"
]
[lifecycle.build]
description = "Build project"
command = "echo 'Building project...'"
cache = true
outputs = ["dist/"]
[lifecycle.test]
description = "Run tests"
command = "echo 'Running tests...'"
[lifecycle.lint]
description = "Lint code"
command = "echo 'Linting...'"
[lifecycle.clean]
description = "Clean artifacts"
command = "echo 'Cleaning...'"
[hooks]
before_build = ["test", "lint"]
after_build = []
"#;
fs::write(temp_dir.path().join("make.toml"), make_toml)
.expect("Failed to write make.toml");
Self {
temp_dir,
state_path,
}
}
fn new_with_workspaces() -> Self {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let state_path = temp_dir.path().join(".ggen/state.json");
fs::create_dir_all(temp_dir.path().join("frontend")).unwrap();
fs::create_dir_all(temp_dir.path().join("backend")).unwrap();
let make_toml = r#"
[project]
name = "monorepo-project"
type = "monorepo"
[workspace.frontend]
path = "frontend"
framework = "react"
runtime = "node"
[workspace.backend]
path = "backend"
framework = "express"
runtime = "node"
[lifecycle.install]
description = "Install dependencies"
command = "echo 'Installing dependencies...'"
[lifecycle.build]
description = "Build workspace"
command = "echo 'Building...'"
"#;
fs::write(temp_dir.path().join("make.toml"), make_toml)
.expect("Failed to write make.toml");
Self {
temp_dir,
state_path,
}
}
fn path(&self) -> &Path {
self.temp_dir.path()
}
fn load_make(&self) -> Make {
load_make(self.path().join("make.toml")).expect("Failed to load make.toml")
}
fn create_context(&self) -> Context {
let make = Arc::new(self.load_make());
Context::new(
self.path().to_path_buf(),
make,
self.state_path.clone(),
vec![],
)
}
fn load_state(&self) -> LifecycleState {
load_state(&self.state_path).expect("Failed to load state")
}
}
#[test]
fn test_load_make_toml() {
let fixture = LifecycleTestFixture::new();
let make = fixture.load_make();
assert_eq!(make.project.name, "test-project");
assert_eq!(make.project.project_type, Some("app".to_string()));
assert_eq!(make.project.version, Some("1.0.0".to_string()));
assert!(make.lifecycle.contains_key("init"));
assert!(make.lifecycle.contains_key("build"));
assert!(make.lifecycle.contains_key("test"));
assert!(make.lifecycle.contains_key("lint"));
assert!(make.lifecycle.contains_key("clean"));
let build_phase = make.lifecycle.get("build").unwrap();
assert_eq!(build_phase.description, Some("Build project".to_string()));
assert_eq!(build_phase.cache, Some(true));
assert_eq!(build_phase.outputs, Some(vec!["dist/".to_string()]));
let hooks = make.hooks.as_ref().unwrap();
assert_eq!(
hooks.before_build,
Some(vec!["test".to_string(), "lint".to_string()])
);
}
#[test]
fn test_run_single_phase() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
let result = run_phase(&ctx, "init");
assert!(result.is_ok(), "Phase execution should succeed");
let state = fixture.load_state();
assert_eq!(state.last_phase, Some("init".to_string()));
assert_eq!(state.phase_history.len(), 1);
let record = &state.phase_history[0];
assert_eq!(record.phase, "init");
assert!(record.success);
assert!(record.duration_ms > 0);
assert!(fixture.path().join("src/test.txt").exists());
}
#[test]
fn test_state_persistence() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
run_phase(&ctx, "init").unwrap();
run_phase(&ctx, "build").unwrap();
let state = fixture.load_state();
assert_eq!(state.last_phase, Some("build".to_string()));
assert_eq!(state.phase_history.len(), 4);
assert_eq!(state.phase_history[0].phase, "init");
assert_eq!(state.phase_history[1].phase, "test");
assert_eq!(state.phase_history[2].phase, "lint");
assert_eq!(state.phase_history[3].phase, "build");
for record in &state.phase_history {
assert!(record.success);
}
}
#[test]
fn test_cache_key_generation() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
run_phase(&ctx, "build").unwrap();
let state = fixture.load_state();
assert!(!state.cache_keys.is_empty());
let build_cache = state
.cache_keys
.iter()
.find(|k| k.phase == "build")
.expect("Build cache key should exist");
assert!(!build_cache.key.is_empty());
assert_eq!(build_cache.key.len(), 64);
let first_build_key = build_cache.key.clone();
run_phase(&ctx, "build").unwrap();
let state2 = fixture.load_state();
let build_caches: Vec<_> = state2
.cache_keys
.iter()
.filter(|k| k.phase == "build")
.collect();
assert!(
build_caches.len() >= 2,
"Should have at least 2 build cache entries"
);
assert_eq!(build_caches[0].key, build_caches[1].key);
assert_eq!(build_caches[0].key, first_build_key);
}
#[test]
fn test_cache_key_changes_with_inputs() {
let cmds = vec!["echo test".to_string()];
let env = vec![];
let inputs = vec![];
let key1 = cache::cache_key("build", &cmds, &env, &inputs);
let key2 = cache::cache_key("test", &cmds, &env, &inputs);
assert_ne!(key1, key2);
let cmds2 = vec!["echo different".to_string()];
let key3 = cache::cache_key("build", &cmds2, &env, &inputs);
assert_ne!(key1, key3);
let env2 = vec![("KEY".to_string(), "value".to_string())];
let key4 = cache::cache_key("build", &cmds, &env2, &inputs);
assert_ne!(key1, key4);
}
#[test]
fn test_hooks_execution_order() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
let result = run_phase(&ctx, "build");
assert!(result.is_ok());
let state = fixture.load_state();
assert_eq!(state.phase_history.len(), 3);
assert_eq!(state.phase_history[0].phase, "test");
assert_eq!(state.phase_history[1].phase, "lint");
assert_eq!(state.phase_history[2].phase, "build");
assert_eq!(state.last_phase, Some("build".to_string()));
}
#[test]
fn test_phase_not_found() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
let result = run_phase(&ctx, "nonexistent");
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("not found"));
}
#[test]
fn test_get_last_run() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
run_phase(&ctx, "init").unwrap();
run_phase(&ctx, "build").unwrap();
run_phase(&ctx, "init").unwrap();
let state = fixture.load_state();
let last_init = state.last_run("init");
assert!(last_init.is_some());
let init_runs: Vec<_> = state
.phase_history
.iter()
.filter(|r| r.phase == "init")
.collect();
assert_eq!(init_runs.len(), 2);
assert_eq!(last_init.unwrap().started_ms, init_runs[1].started_ms);
}
#[test]
fn test_get_cache_key() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
run_phase(&ctx, "build").unwrap();
let state = fixture.load_state();
let cache_key = state.get_cache_key("build");
assert!(cache_key.is_some());
assert_eq!(cache_key.unwrap().len(), 64);
assert!(state.get_cache_key("nonexistent").is_none());
}
#[test]
fn test_pipeline_execution() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
let phases = vec!["init".to_string(), "build".to_string(), "test".to_string()];
let result = exec::run_pipeline(&ctx, &phases);
if let Err(e) = &result {
log::error!("Pipeline execution failed: {:?}", e);
}
assert!(result.is_ok());
let state = fixture.load_state();
assert!(state.phase_history.len() >= 3);
assert_eq!(state.phase_history[0].phase, "init");
assert_eq!(state.last_phase, Some("test".to_string()));
}
#[test]
fn test_workspace_support() {
let fixture = LifecycleTestFixture::new_with_workspaces();
let make = fixture.load_make();
assert!(make.workspace.is_some());
let workspaces = make.workspace.as_ref().unwrap();
assert_eq!(workspaces.len(), 2);
assert!(workspaces.contains_key("frontend"));
assert!(workspaces.contains_key("backend"));
let frontend = workspaces.get("frontend").unwrap();
assert_eq!(frontend.path, "frontend");
assert_eq!(frontend.framework, Some("react".to_string()));
assert_eq!(frontend.runtime, Some("node".to_string()));
}
#[test]
fn test_phase_commands_extraction() {
let fixture = LifecycleTestFixture::new();
let make = fixture.load_make();
let build_cmds = make.phase_commands("build");
assert_eq!(build_cmds.len(), 1);
assert_eq!(build_cmds[0], "echo 'Building project...'");
let init_cmds = make.phase_commands("init");
assert_eq!(init_cmds.len(), 2);
assert_eq!(init_cmds[0], "mkdir -p src");
assert_eq!(init_cmds[1], "echo 'test' > src/test.txt");
let empty = make.phase_commands("nonexistent");
assert_eq!(empty.len(), 0);
}
#[test]
fn test_phase_names_listing() {
let fixture = LifecycleTestFixture::new();
let make = fixture.load_make();
let phases = make.phase_names();
assert_eq!(phases.len(), 5);
assert!(phases.contains(&"init".to_string()));
assert!(phases.contains(&"build".to_string()));
assert!(phases.contains(&"test".to_string()));
assert!(phases.contains(&"lint".to_string()));
assert!(phases.contains(&"clean".to_string()));
}
#[test]
fn test_multiple_phase_runs_state_history() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
for _ in 0..3 {
run_phase(&ctx, "test").unwrap();
}
let state = fixture.load_state();
assert_eq!(state.phase_history.len(), 3);
for record in &state.phase_history {
assert_eq!(record.phase, "test");
assert!(record.success);
}
}
#[test]
fn test_cache_storage_and_validation() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().join(".ggen/cache");
let phase = "build";
let key = "test_cache_key_123";
assert!(!cache::is_cache_valid(&cache_dir, phase, key));
cache::store_cache(&cache_dir, phase, key).unwrap();
assert!(cache::is_cache_valid(&cache_dir, phase, key));
let cache_path = cache_dir.join(phase).join(key);
assert!(cache_path.exists());
}
#[test]
fn test_empty_phase_commands() {
let temp_dir = TempDir::new().unwrap();
let make_toml = r#"
[project]
name = "test"
[lifecycle.empty]
description = "Phase with no commands"
"#;
fs::write(temp_dir.path().join("make.toml"), make_toml).unwrap();
let result = load_make(temp_dir.path().join("make.toml"));
assert!(result.is_err());
match result {
Err(super::super::error::LifecycleError::NoCommands { phase }) => {
assert_eq!(phase, "empty");
}
_ => panic!("Expected NoCommands error"),
}
}
#[test]
fn test_load_make_or_default() {
let temp_dir = TempDir::new().unwrap();
let make = loader::load_make_or_default(temp_dir.path()).unwrap();
assert_eq!(
make.project.name,
crate::lifecycle::model::defaults::DEFAULT_PROJECT_NAME
);
assert_eq!(
make.project.version,
Some(crate::lifecycle::model::defaults::DEFAULT_PROJECT_VERSION.to_string())
);
assert_eq!(make.lifecycle.len(), 0);
}
#[test]
fn test_concurrent_phase_execution_state() {
let fixture = LifecycleTestFixture::new();
let ctx = fixture.create_context();
let phases = vec!["init", "test", "lint", "clean"];
for phase in phases {
run_phase(&ctx, phase).unwrap();
}
let state = fixture.load_state();
assert_eq!(state.phase_history.len(), 4);
let phase_names: Vec<_> = state
.phase_history
.iter()
.map(|r| r.phase.as_str())
.collect();
assert!(phase_names.contains(&"init"));
assert!(phase_names.contains(&"test"));
assert!(phase_names.contains(&"lint"));
assert!(phase_names.contains(&"clean"));
}
#[test]
fn test_parallel_workspace_execution() {
use std::time::Instant;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
for i in 1..=3 {
let ws_path = temp_dir.path().join(format!("workspace{}", i));
fs::create_dir_all(&ws_path).unwrap();
let make_toml = format!(
r#"
[project]
name = "workspace{}"
[lifecycle.timestamp]
description = "Write timestamp"
command = "date +%s%N > timestamp.txt"
"#,
i
);
fs::write(ws_path.join("make.toml"), make_toml).unwrap();
}
let root_make = r#"
[project]
name = "parallel-test"
type = "monorepo"
[workspace.workspace1]
path = "workspace1"
[workspace.workspace2]
path = "workspace2"
[workspace.workspace3]
path = "workspace3"
[lifecycle.timestamp]
description = "Write timestamp"
parallel = true
command = "date +%s%N > timestamp.txt"
"#;
fs::write(temp_dir.path().join("make.toml"), root_make).unwrap();
let make = Arc::new(load_make(temp_dir.path().join("make.toml")).unwrap());
let state_path = temp_dir.path().join(".ggen/state.json");
let ctx = Context::new(temp_dir.path().to_path_buf(), make, state_path, vec![]);
let start = Instant::now();
exec::run_pipeline(&ctx, &["timestamp".to_string()]).unwrap();
let duration = start.elapsed();
for i in 1..=3 {
let timestamp_file = temp_dir
.path()
.join(format!("workspace{}/timestamp.txt", i));
assert!(
timestamp_file.exists(),
"Workspace {} timestamp file should exist",
i
);
}
assert!(
duration.as_millis() < 5000,
"Parallel execution took too long: {}ms",
duration.as_millis()
);
for i in 1..=3 {
let ws_state_path = temp_dir
.path()
.join(format!("workspace{}/.ggen/state.json", i));
assert!(
ws_state_path.exists(),
"Workspace {} should have state file",
i
);
let ws_state = load_state(&ws_state_path).expect("Failed to load workspace state");
assert_eq!(ws_state.last_phase, Some("timestamp".to_string()));
}
}
#[test]
fn test_parallel_workspace_error_handling() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
for i in 1..=3 {
let ws_path = temp_dir.path().join(format!("workspace{}", i));
fs::create_dir_all(&ws_path).unwrap();
let command = if i == 2 {
"exit 1"
} else {
"echo 'success' > result.txt"
};
let make_toml = format!(
r#"
[project]
name = "workspace{}"
[lifecycle.process]
description = "Process"
command = "{}"
"#,
i, command
);
fs::write(ws_path.join("make.toml"), make_toml).unwrap();
}
let root_make = r#"
[project]
name = "error-test"
type = "monorepo"
[workspace.workspace1]
path = "workspace1"
[workspace.workspace2]
path = "workspace2"
[workspace.workspace3]
path = "workspace3"
[lifecycle.process]
description = "Process"
parallel = true
command = "echo 'test'"
"#;
fs::write(temp_dir.path().join("make.toml"), root_make).unwrap();
let make = Arc::new(load_make(temp_dir.path().join("make.toml")).unwrap());
let state_path = temp_dir.path().join(".ggen/state.json");
let ctx = Context::new(temp_dir.path().to_path_buf(), make, state_path, vec![]);
let result = exec::run_pipeline(&ctx, &["process".to_string()]);
assert!(result.is_err(), "Pipeline should fail when workspace fails");
}
}