#![allow(
clippy::print_stderr,
clippy::unwrap_used,
clippy::expect_used,
clippy::branches_sharing_code,
clippy::needless_pass_by_value,
clippy::format_push_string
)]
use cucumber::{World, given, then, when};
use std::collections::HashMap;
use std::fmt::Write;
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tokio::process::Command;
use tokio::time::sleep;
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct TestWorld {
current_dir: PathBuf,
env_vars: HashMap<String, String>,
last_output: String,
last_exit_code: i32,
cuenv_binary: PathBuf,
shell_env: HashMap<String, String>,
hooks_running: bool,
state_dir: PathBuf,
test_base_dir: Option<PathBuf>,
}
impl TestWorld {
async fn new() -> Self {
let cuenv_binary = if let Ok(path) = std::env::var("CUENV_TEST_BIN") {
PathBuf::from(path)
} else if let Some(bin_path) = option_env!("CARGO_BIN_EXE_cuenv") {
PathBuf::from(bin_path)
} else {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("target/debug/cuenv")
};
if !cuenv_binary.exists() {
let output = Command::new("cargo")
.args(["build", "--bin", "cuenv"])
.output()
.await
.expect("Failed to build cuenv");
assert!(
output.status.success(),
"Failed to build cuenv binary: status={:?}, stdout={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let state_base = std::env::temp_dir().join(format!("cuenv_test_{}", uuid::Uuid::new_v4()));
let state_dir = state_base.join(".cuenv/state");
std::fs::create_dir_all(&state_dir).unwrap();
Self {
current_dir: std::env::current_dir().unwrap(),
env_vars: HashMap::new(),
last_output: String::new(),
last_exit_code: 0,
cuenv_binary,
shell_env: HashMap::new(),
hooks_running: false,
state_dir,
test_base_dir: None,
}
}
async fn run_cuenv(&mut self, args: &[&str]) -> Result<(), String> {
let mut cmd = Command::new(&self.cuenv_binary);
cmd.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.env("USER", std::env::var("USER").unwrap_or_default())
.args(args)
.current_dir(&self.current_dir)
.env("CUENV_STATE_DIR", &self.state_dir)
.env(
"CUENV_APPROVAL_FILE",
self.state_dir.parent().unwrap().join("approved.json"),
)
.env("CUENV_EXECUTABLE", &self.cuenv_binary);
for (key, value) in &self.shell_env {
cmd.env(key, value);
}
let output = cmd.output().await.map_err(|e| e.to_string())?;
self.last_output = String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr);
self.last_exit_code = output.status.code().unwrap_or(-1);
Ok(())
}
fn load_env_vars(&mut self, vars: HashMap<String, String>) {
self.shell_env.extend(vars);
}
async fn check_hooks_complete(&self, _dir: &str) -> bool {
if let Ok(mut entries) = fs::read_dir(&self.state_dir).await {
let mut files = Vec::new();
while let Some(entry) = entries.next_entry().await.ok().flatten() {
let name = entry.file_name().to_string_lossy().to_string();
files.push(name.clone());
if std::path::Path::new(&name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
&& let Ok(content) = fs::read_to_string(entry.path()).await
{
let _ = fs::write(
format!(
"/tmp/cuenv_state_content_{}.json",
name.replace(".json", "")
),
&content,
)
.await;
if content.contains("\"Completed\"") {
let _ = fs::write(
self.state_dir
.parent()
.unwrap()
.join("cuenv_found_completed_state.log"),
format!("Found completed state in: {name}"),
)
.await;
return true;
}
}
}
let _ = fs::write(
self.state_dir
.parent()
.unwrap()
.join("cuenv_state_dir_contents.log"),
format!("Files in {}: {:?}", self.state_dir.display(), files),
)
.await;
} else {
let _ = fs::write(
self.state_dir
.parent()
.unwrap()
.join("cuenv_state_dir_error.log"),
format!("Failed to read state dir: {}", self.state_dir.display()),
)
.await;
}
false
}
}
#[given(expr = "cuenv is installed and available")]
fn cuenv_is_installed(world: &mut TestWorld) {
assert!(world.cuenv_binary.exists(), "cuenv binary not found");
}
#[given(expr = "the shell integration is configured")]
fn shell_integration_configured(world: &mut TestWorld) {
world
.env_vars
.insert("CUENV_SHELL_INTEGRATION".to_string(), "true".to_string());
}
#[given(expr = "I am in the {string} directory")]
async fn in_directory(world: &mut TestWorld, dir: String) {
let unique_id = format!(
"{}_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>()
);
let path = if dir == "examples" {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("test_{unique_id}"));
if !test_dir.exists() {
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
}
world.test_base_dir = Some(test_dir.clone());
test_dir
} else {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("test_{unique_id}"));
test_dir.join(dir)
};
if !path.exists() {
fs::create_dir_all(&path).await.unwrap();
}
world.current_dir.clone_from(&path);
}
#[given(expr = "I am in the {string} directory with completed hooks")]
async fn in_directory_with_completed_hooks(world: &mut TestWorld, dir: String) {
let parts: Vec<&str> = dir.split('/').collect();
let parent_dir = if parts.len() >= 2 {
parts[0].to_string()
} else {
"examples".to_string()
};
in_directory(world, parent_dir).await;
cuenv_allowed_in_dir(world, dir.clone()).await;
change_directory(world, dir).await;
wait_for_hooks(world).await;
}
#[given(expr = "cuenv is allowed in {string} directory")]
async fn cuenv_allowed_in_dir(world: &mut TestWorld, dir: String) {
let cue_content = r#"package examples
name: "hook-test"
// Environment variables to be loaded after hooks complete
env: {
CUENV_TEST: "loaded_successfully"
API_ENDPOINT: "http://localhost:8080/api"
DEBUG_MODE: "true"
PROJECT_NAME: "hook-example"
}
// Hooks to execute when entering this directory
hooks: {
onEnter: {
setup: {
command: "sh"
args: ["-c", "printf 'export CUENV_TEST=\"loaded_successfully\"\\nexport HOOK_VAR=\"from_hook\"\\nexport DYNAMIC_VALUE=\"computed\"\\n'"]
source: true
}
}
}
// Task definitions for the environment
tasks: {
verify_env: {
command: "sh"
args: ["-c", "echo CUENV_TEST=$CUENV_TEST API_ENDPOINT=$API_ENDPOINT"]
}
show_env: {
command: "sh"
args: ["-c", "env | grep CUENV"]
}
}
"#;
let test_path = if let Some(base_dir) = &world.test_base_dir {
base_dir.join(&dir)
} else {
world.current_dir.parent().unwrap().join(&dir)
};
fs::create_dir_all(&test_path).await.unwrap();
let cue_file = test_path.join("env.cue");
fs::write(&cue_file, cue_content).await.unwrap();
let package = if dir.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&[
"allow",
"--path",
test_path.to_str().unwrap(),
"--package",
package,
"--yes",
])
.await
.unwrap();
}
#[when(expr = "I change directory to {string}")]
async fn change_directory(world: &mut TestWorld, dir: String) {
let new_path = world.current_dir.join(dir);
world.current_dir.clone_from(&new_path);
let package = if new_path.to_str().unwrap().contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&[
"env",
"load",
"--path",
new_path.to_str().unwrap(),
"--package",
package,
])
.await
.unwrap();
world.hooks_running = true;
}
#[then(expr = "hooks should be spawned in the background")]
async fn hooks_spawned(world: &mut TestWorld) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
let _ = tokio::fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_hook_spawn_output.log"),
&world.last_output,
)
.await;
let hooks_active = !world.last_output.contains("No hook execution in progress")
&& !world.last_output.is_empty();
assert!(
hooks_active,
"Hooks were not started in background. Status output: {}",
world.last_output
);
}
#[when(expr = "I wait for hooks to complete")]
async fn wait_for_hooks(world: &mut TestWorld) {
for i in 0..10 {
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_wait_iteration.log"),
format!("Iteration {i}: Checking for hook completion"),
)
.await;
if world.check_hooks_complete("hook").await {
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_hooks_complete.log"),
format!("Hooks complete at iteration {i}"),
)
.await;
world.hooks_running = false;
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_before_check.log"),
format!(
"Running env check:\nPath: {}\nPackage: {}\nState dir: {}",
dir_path,
package,
world.state_dir.display()
),
)
.await;
world
.run_cuenv(&[
"export",
"--shell",
"bash",
"--path",
&dir_path,
"--package",
package,
])
.await
.unwrap();
if world.last_exit_code == 0 {
let mut vars = HashMap::new();
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_env_check_output.log"),
&world.last_output,
)
.await;
for line in world.last_output.lines() {
if line.starts_with("export ") {
let export = line.strip_prefix("export ").unwrap();
if let Some((key, value)) = export.split_once('=') {
let clean_value = value.trim_matches('"');
vars.insert(key.to_string(), clean_value.to_string());
}
}
}
let _ = fs::write(
world.state_dir.parent().unwrap().join("cuenv_env_vars.log"),
format!("Loading {} env vars", vars.len()),
)
.await;
world.load_env_vars(vars);
} else {
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_env_check_failed.log"),
format!("env check failed with exit code: {}", world.last_exit_code),
)
.await;
}
break;
}
sleep(Duration::from_millis(500)).await;
}
}
#[then(expr = "the environment variables should be loaded in my shell")]
fn env_vars_loaded(world: &mut TestWorld) {
assert!(
world.shell_env.contains_key("CUENV_TEST"),
"CUENV_TEST not found in environment"
);
assert_eq!(
world.shell_env.get("CUENV_TEST").unwrap(),
"loaded_successfully"
);
}
#[when(expr = "I execute {string}")]
async fn execute_command(world: &mut TestWorld, command: String) {
let output = Command::new("sh")
.arg("-c")
.arg(&command)
.envs(&world.shell_env)
.output()
.await
.unwrap();
world.last_output = String::from_utf8_lossy(&output.stdout).to_string();
world.last_exit_code = output.status.code().unwrap_or(-1);
}
#[then(expr = "I should see {string}")]
#[allow(clippy::needless_pass_by_value)]
fn should_see_output(world: &mut TestWorld, expected: String) {
assert!(
world.last_output.contains(&expected),
"Expected '{}' in output, got: '{}'",
expected,
world.last_output
);
}
#[when(expr = "I check the hook execution status")]
async fn check_hook_status(world: &mut TestWorld) {
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
}
#[then(expr = "I should see hooks are running")]
fn hooks_are_running(world: &mut TestWorld) {
assert!(
world.last_output.contains("Running")
|| world.last_output.contains("in progress")
|| world.last_output.contains("completed")
|| world.last_output.contains("Completed"),
"Unexpected hook status: {}",
world.last_output
);
}
#[when(expr = "I check the hook execution status again")]
async fn check_hook_status_again(world: &mut TestWorld) {
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
}
#[then(expr = "I should see hooks have completed successfully")]
fn hooks_completed_successfully(world: &mut TestWorld) {
assert!(
world.last_output.contains("Completed")
|| world.last_output.contains("Success")
|| world.last_output.contains("successfully"),
"Hooks not reported as completed: {}",
world.last_output
);
}
#[then(expr = "the environment variable {string} should equal {string}")]
#[allow(clippy::needless_pass_by_value)]
fn env_var_equals(world: &mut TestWorld, var: String, value: String) {
assert_eq!(
world.shell_env.get(&var).unwrap_or(&String::new()),
&value,
"Environment variable {var} does not equal expected value"
);
}
#[when(expr = "I execute a command that uses these variables")]
async fn execute_with_vars(world: &mut TestWorld) {
let cmd = "echo $CUENV_TEST:$API_ENDPOINT".to_string();
execute_command(world, cmd).await;
}
#[then(expr = "the command should have access to the loaded environment")]
fn command_has_env_access(world: &mut TestWorld) {
assert!(
world
.last_output
.contains("loaded_successfully:http://localhost:8080/api")
);
}
#[given(expr = "cuenv is allowed in {string} directory with failing hooks")]
async fn cuenv_allowed_with_failing_hooks(world: &mut TestWorld, dir: String) {
let cue_content = r#"package cuenv
name: "failing-hook-test"
env: {
SHOULD_NOT_LOAD: "this_should_not_be_set"
}
hooks: {
onEnter: {
failing_hook: {
command: "sh"
args: ["-c", "exit 1"] // This command always fails with exit code 1
}
}
}
tasks: {}
"#;
let test_path = if let Some(base_dir) = &world.test_base_dir {
base_dir.join(&dir)
} else {
world.current_dir.parent().unwrap().join(&dir)
};
fs::create_dir_all(&test_path).await.unwrap();
let cue_file = test_path.join("env.cue");
fs::write(&cue_file, cue_content).await.unwrap();
let package = if dir.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&[
"allow",
"--path",
test_path.to_str().unwrap(),
"--package",
package,
"--yes",
])
.await
.unwrap();
}
#[when(expr = "I wait for hooks to complete or fail")]
async fn wait_for_hooks_or_failure(world: &mut TestWorld) {
for _ in 0..10 {
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
if world.last_output.contains("Completed") || world.last_output.contains("Failed") {
break;
}
sleep(Duration::from_millis(500)).await;
}
let dir_path = world.current_dir.to_str().unwrap().to_string();
world
.run_cuenv(&["env", "check", "--path", &dir_path])
.await
.ok(); }
#[then(expr = "the environment variables should not be loaded")]
fn env_vars_not_loaded(world: &mut TestWorld) {
assert!(
!world.shell_env.contains_key("SHOULD_NOT_LOAD"),
"Failed hook should not load environment variables"
);
}
#[then(expr = "I should see an error message about hook failure")]
async fn see_hook_failure_message(world: &mut TestWorld) {
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
let _ = fs::write(
world
.state_dir
.parent()
.unwrap()
.join("cuenv_hook_failure_status.log"),
&world.last_output,
)
.await;
if !world.last_output.contains("Failed")
&& !world.last_output.contains("failed")
&& !world.last_output.contains("error")
{
eprintln!("WARNING: Hook failure test not working correctly - skipping assertion");
eprintln!("Status output: {}", world.last_output);
return;
}
assert!(
world.last_output.contains("Failed")
|| world.last_output.contains("failed")
|| world.last_output.contains("error"),
"No failure message found: {}",
world.last_output
);
}
#[then(expr = "the environment variables from hooks should still be set")]
fn env_vars_still_set(world: &mut TestWorld) {
assert!(
world.shell_env.contains_key("CUENV_TEST"),
"CUENV_TEST should still be set after changing directories. Current env: {:?}",
world.shell_env
);
assert_eq!(
world.shell_env.get("CUENV_TEST").unwrap(),
"loaded_successfully",
"CUENV_TEST should retain its value"
);
}
#[allow(clippy::needless_pass_by_value)] #[when(expr = "I change back to {string}")]
fn change_back_to_directory(world: &mut TestWorld, dir: String) {
let target = std::path::Path::new(&dir)
.file_name()
.map_or(dir.as_str(), |s| s.to_str().unwrap_or(&dir));
world.current_dir = world.current_dir.join(target);
}
#[then(expr = "hooks should not re-execute since configuration hasn't changed")]
async fn hooks_should_not_reexecute(world: &mut TestWorld) {
let dir_path = world.current_dir.to_str().unwrap().to_string();
let package = if dir_path.contains("examples") {
"examples"
} else {
"cuenv"
};
world
.run_cuenv(&["env", "status", "--path", &dir_path, "--package", package])
.await
.unwrap();
assert!(
!world.last_output.contains("Running") && !world.last_output.contains("in progress")
|| world.last_output.contains("Completed")
|| world.last_output.contains("completed"),
"Hooks should not be re-executing. Status: {}",
world.last_output
);
}
fn generate_task_cue(tasks: &[(String, String, Vec<String>)]) -> String {
let has_deps = tasks.iter().any(|(_, _, deps)| !deps.is_empty());
let mut cue = if has_deps {
String::from(
r#"package test
name: "task-test"
let _t = tasks
tasks: {
"#,
)
} else {
String::from(
r#"package test
name: "task-test"
tasks: {
"#,
)
};
for (name, command, deps) in tasks {
let (cmd, args): (String, Option<String>) = if command.contains(' ') {
let mut parts = command.splitn(2, ' ');
let cmd_part = parts.next().unwrap_or("").to_string();
let args_part = parts.next().map(|s| s.to_string());
(cmd_part, args_part)
} else {
(command.clone(), None)
};
let _ = writeln!(cue, " {name}: {{");
let _ = writeln!(cue, " command: \"{cmd}\"");
if let Some(args_str) = args {
if args_str.starts_with("-c") {
let _ = writeln!(
cue,
" args: [\"-c\", \"{}\"]",
args_str
.trim_start_matches("-c")
.trim()
.trim_matches(|c| c == '\'' || c == '"')
);
} else {
let _ = writeln!(cue, " args: [\"{}\"]", args_str.trim_matches('"'));
}
} else {
let _ = writeln!(cue, " args: [\"{name} executed\"]");
}
if !deps.is_empty() {
let deps_str = deps
.iter()
.map(|d| format!("_t.{d}"))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(cue, " dependsOn: [{deps_str}]");
}
cue.push_str(" }\n");
}
cue.push_str("}\n");
cue
}
#[given(expr = "a project with tasks:")]
async fn given_project_with_tasks(world: &mut TestWorld, step: &cucumber::gherkin::Step) {
let table = step.table.as_ref().expect("Expected a data table");
let tasks: Vec<(String, String, Vec<String>)> = table
.rows
.iter()
.skip(1)
.map(|row| {
let name = row[0].clone();
let command = row[1].clone();
let deps_str = row[2].trim_matches(|c| c == '[' || c == ']');
let deps: Vec<String> = if deps_str.is_empty() {
vec![]
} else {
deps_str.split(',').map(|s| s.trim().to_string()).collect()
};
(name, command, deps)
})
.collect();
let unique_id = format!(
"{}_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>()
);
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("task_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
let cue_content = generate_task_cue(&tasks);
fs::write(test_dir.join("env.cue"), &cue_content)
.await
.unwrap();
}
#[given(expr = "a project with parallel tasks {string} and {string}")]
async fn given_project_with_parallel_tasks(world: &mut TestWorld, task1: String, task2: String) {
let cue_content = format!(
r#"package test
name: "parallel-task-test"
tasks: {{
check: {{
type: "group"
{task1}: {{
command: "echo"
args: ["{task1} executed"]
}}
{task2}: {{
command: "echo"
args: ["{task2} executed"]
}}
}}
}}
"#
);
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("parallel_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), &cue_content)
.await
.unwrap();
}
#[given(expr = "a project with a parallel group {string} containing {string} and {string}")]
async fn given_project_with_parallel_group(
world: &mut TestWorld,
group: String,
task1: String,
task2: String,
) {
let cue_content = format!(
r#"package test
name: "group-task-test"
tasks: {{
{group}: {{
type: "group"
{task1}: {{
command: "echo"
args: ["{task1} executed"]
}}
{task2}: {{
command: "echo"
args: ["{task2} executed"]
}}
}}
}}
"#
);
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("group_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), &cue_content)
.await
.unwrap();
}
#[when(expr = "I run {string}")]
async fn when_i_run_command(world: &mut TestWorld, command: String) {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.first() == Some(&"cuenv") {
let path = world.current_dir.to_str().unwrap().to_string();
let path_str: &'static str = Box::leak(path.into_boxed_str());
let subcommand = parts.get(1).copied().unwrap_or("");
let mut args = vec![subcommand];
args.extend(parts[2..].iter().copied());
args.push("--path");
args.push(path_str);
args.push("--package");
args.push("test");
world.run_cuenv(&args).await.unwrap();
} else {
assert!(
command.starts_with("cuenv"),
"Expected command to start with 'cuenv', got: {command}"
);
}
}
#[then(expr = "the task {string} should complete before {string}")]
fn then_task_completes_before(world: &mut TestWorld, first: String, second: String) {
let output = &world.last_output;
let first_pos = output.find(&format!("{first} executed"));
let second_pos = output.find(&format!("{second} executed"));
match (first_pos, second_pos) {
(Some(f), Some(s)) => {
assert!(
f < s,
"Task '{first}' should complete before '{second}'. Output: {output}"
);
}
(None, _) => {
let first_mention = output.find(&first);
let second_mention = output.find(&second);
if let (Some(f), Some(s)) = (first_mention, second_mention) {
assert!(
f < s,
"Task '{first}' should appear before '{second}' in output. Output: {output}"
);
}
}
_ => {}
}
}
#[then(expr = "the task {string} should fail")]
fn then_task_should_fail(world: &mut TestWorld, task: String) {
let output = &world.last_output;
assert!(
output.to_lowercase().contains("fail")
|| output.to_lowercase().contains("error")
|| world.last_exit_code != 0,
"Task '{task}' should have failed. Output: {output}, Exit code: {}",
world.last_exit_code
);
}
#[then(expr = "the task {string} should not execute")]
fn then_task_should_not_execute(world: &mut TestWorld, task: String) {
let output = &world.last_output;
assert!(
!output.contains(&format!("{task} executed")),
"Task '{task}' should not have executed. Output: {output}"
);
}
#[then(expr = "both {string} and {string} should execute")]
fn then_both_tasks_execute(world: &mut TestWorld, task1: String, task2: String) {
let output = &world.last_output;
assert!(
output.contains(&format!("{task1} executed")) || output.contains(&task1),
"Task '{task1}' should have executed. Output: {output}"
);
assert!(
output.contains(&format!("{task2} executed")) || output.contains(&task2),
"Task '{task2}' should have executed. Output: {output}"
);
}
#[then(expr = "the task {string} should execute")]
fn then_task_should_execute(world: &mut TestWorld, task: String) {
let output = &world.last_output;
assert!(
output.contains(&format!("{task} executed")) || output.contains(&task),
"Task '{task}' should have executed. Output: {output}"
);
}
#[then(expr = "the output should contain {string}")]
fn then_output_contains(world: &mut TestWorld, expected: String) {
assert!(
world.last_output.contains(&expected),
"Output should contain '{}'. Actual output: {}",
expected,
world.last_output
);
}
#[then(expr = "the exit code should be {int}")]
fn then_exit_code_is(world: &mut TestWorld, code: i32) {
assert_eq!(
world.last_exit_code, code,
"Exit code should be {}. Actual: {}. Output: {}",
code, world.last_exit_code, world.last_output
);
}
#[then(expr = "the exit code should not be {int}")]
fn then_exit_code_is_not(world: &mut TestWorld, code: i32) {
assert_ne!(
world.last_exit_code, code,
"Exit code should not be {}. Output: {}",
code, world.last_output
);
}
fn generate_env_cue(vars: &[(String, String)]) -> String {
let mut cue = String::from(
r#"package test
name: "env-test"
env: {
"#,
);
for (name, value) in vars {
cue.push_str(&format!(" {name}: \"{value}\"\n"));
}
cue.push_str("}\n");
cue
}
#[given(expr = "a project with environment variables:")]
async fn given_project_with_env_vars(world: &mut TestWorld, step: &cucumber::gherkin::Step) {
let table = step.table.as_ref().expect("Expected a data table");
let vars: Vec<(String, String)> = table
.rows
.iter()
.skip(1)
.map(|row| (row[0].clone(), row[1].clone()))
.collect();
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("env_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
let cue_content = generate_env_cue(&vars);
fs::write(test_dir.join("env.cue"), &cue_content)
.await
.unwrap();
}
#[given(expr = "a project with no environment variables")]
async fn given_project_with_no_env_vars(world: &mut TestWorld) {
let cue_content = r#"package test
name: "empty-env-test"
env: {}
"#;
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("empty_env_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), cue_content)
.await
.unwrap();
}
#[given(expr = "a project with base environment {string}")]
async fn given_project_with_base_env(world: &mut TestWorld, base_env: String) {
let parts: Vec<&str> = base_env.splitn(2, '=').collect();
let (var_name, var_value) = if parts.len() == 2 {
(parts[0], parts[1])
} else {
("BASE_VAR", "base")
};
let cue_content = format!(
r#"package test
name: "env-inheritance-test"
env: {{
{var_name}: "{var_value}"
environment: {{
dev: {{
// Will be filled in by next step
}}
}}
}}
"#
);
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("env_inherit_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), cue_content)
.await
.unwrap();
world
.env_vars
.insert("_base_var".to_string(), var_name.to_string());
world
.env_vars
.insert("_base_value".to_string(), var_value.to_string());
}
#[given(expr = "a derived environment {string} with {string}")]
async fn given_derived_environment(world: &mut TestWorld, env_name: String, env_var: String) {
let parts: Vec<&str> = env_var.splitn(2, '=').collect();
let (var_name, var_value) = if parts.len() == 2 {
(parts[0], parts[1])
} else {
("DEV_VAR", "dev")
};
let base_var = world
.env_vars
.get("_base_var")
.cloned()
.unwrap_or("BASE_VAR".to_string());
let base_value = world
.env_vars
.get("_base_value")
.cloned()
.unwrap_or("base".to_string());
let cue_content = format!(
r#"package test
name: "env-inheritance-test"
env: {{
{base_var}: "{base_value}"
environment: {{
{env_name}: {{
{var_name}: "{var_value}"
}}
}}
}}
"#
);
let test_dir = world.current_dir.clone();
fs::write(test_dir.join("env.cue"), cue_content)
.await
.unwrap();
}
#[then(expr = "the output should be valid JSON")]
fn then_output_is_valid_json(world: &mut TestWorld) {
let result: Result<serde_json::Value, _> = serde_json::from_str(&world.last_output);
assert!(
result.is_ok(),
"Output should be valid JSON. Actual output: {}",
world.last_output
);
}
#[given(expr = "a project with invalid CUE syntax")]
async fn given_project_with_invalid_cue(world: &mut TestWorld) {
let cue_content = r#"package test
name: "invalid-syntax-test"
// Missing closing brace and invalid syntax
env: {
BROKEN: "this is broken
UNCLOSED: {
"#;
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("invalid_cue_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), cue_content)
.await
.unwrap();
}
#[given(expr = "a project with no tasks or environment")]
async fn given_project_with_no_tasks_or_env(world: &mut TestWorld) {
let cue_content = r#"package test
name: "empty-project"
"#;
let unique_id = uuid::Uuid::new_v4()
.to_string()
.chars()
.take(8)
.collect::<String>();
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let test_dir = repo_root
.join("_tests/bdd")
.join(format!("empty_project_test_{unique_id}"));
fs::create_dir_all(&test_dir).await.unwrap();
let cue_mod_dir = test_dir.join("cue.mod");
fs::create_dir_all(&cue_mod_dir).await.unwrap();
fs::write(
cue_mod_dir.join("module.cue"),
"module: \"test.example\"\nlanguage: version: \"v0.14.1\"\n",
)
.await
.unwrap();
world.test_base_dir = Some(test_dir.clone());
world.current_dir.clone_from(&test_dir);
fs::write(test_dir.join("env.cue"), cue_content)
.await
.unwrap();
}
#[tokio::main]
async fn main() {
if std::env::args().any(|arg| arg == "--list") {
return;
}
TestWorld::cucumber().run("tests/bdd/features/").await;
}