use crate::allowlist;
use crate::task_discovery;
use crate::types::AllowScope;
use std::env;
pub fn execute(task_with_args: &str, allow: Option<u8>) -> Result<(), String> {
let task_name = task_with_args
.split_whitespace()
.next()
.ok_or_else(|| "No task name provided".to_string())?;
let current_dir =
env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
let discovered = task_discovery::discover_tasks(¤t_dir);
let matching_tasks = task_discovery::get_matching_tasks(&discovered, task_name);
match matching_tasks.len() {
0 => Err(format!("dela: command or task not found: {}", task_name)),
1 => {
let task = matching_tasks[0];
if let Some(choice) = allow {
match choice {
2 => {
allowlist::check_task_allowed_with_scope(task, AllowScope::Task)?;
Ok(())
}
3 => {
allowlist::check_task_allowed_with_scope(task, AllowScope::File)?;
Ok(())
}
4 => {
allowlist::check_task_allowed_with_scope(task, AllowScope::Directory)?;
Ok(())
}
5 => {
eprintln!("Task '{}' was denied by the allowlist.", task.name);
Err(format!(
"Dela task '{}' was denied by the ~/.dela/allowlist.toml",
task.name
))
}
_ => Err(format!(
"Invalid allow choice {}. Please use a number between 2 and 5.",
choice
)),
}
} else {
if !allowlist::check_task_allowed(task)? {
eprintln!("Task '{}' was denied by the allowlist.", task.name);
return Err(format!(
"Dela task '{}' was denied by the ~/.dela/allowlist.toml",
task.name
));
}
Ok(())
}
}
_ => {
let error_msg = task_discovery::format_ambiguous_task_error(task_name, &matching_tasks);
eprintln!("{}", error_msg);
Err(format!("Multiple tasks named '{}' found", task_name))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs::{self, File};
use std::io::Write;
use tempfile::TempDir;
fn with_stdin<F>(input: &str, test: F)
where
F: FnOnce(),
{
use std::fs::File;
use std::io::Write;
use std::os::unix::io::FromRawFd;
unsafe {
let mut pipe = [0; 2];
libc::pipe(&mut pipe[0]);
let mut writer = File::from_raw_fd(pipe[1]);
writer.write_all(input.as_bytes()).unwrap();
drop(writer);
let old_stdin = libc::dup(0);
libc::dup2(pipe[0], 0);
test();
libc::dup2(old_stdin, 0);
libc::close(old_stdin);
libc::close(pipe[0]);
}
}
fn setup_test_env() -> (TempDir, TempDir) {
let project_dir = TempDir::new().expect("Failed to create temp directory");
let makefile_content = "
build: ## Building the project
\t@echo Building...
test: ## Running tests
\t@echo Testing...
";
let mut makefile =
File::create(project_dir.path().join("Makefile")).expect("Failed to create Makefile");
makefile
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");
let home_dir = TempDir::new().expect("Failed to create temp HOME directory");
env::set_var("HOME", home_dir.path());
fs::create_dir_all(home_dir.path().join(".dela"))
.expect("Failed to create .dela directory");
(project_dir, home_dir)
}
#[test]
#[serial]
fn test_allow_command_single_task() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
with_stdin("1\n", || {
let result = execute("test", None);
assert!(result.is_ok(), "Should succeed for a single task");
});
}
#[test]
#[serial]
fn test_allow_command_with_args() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
with_stdin("1\n", || {
let result = execute("test --verbose --coverage", None);
assert!(result.is_ok(), "Should succeed for task with arguments");
});
}
#[test]
#[serial]
fn test_allow_command_denied_task() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
with_stdin("5\n", || {
let result = execute("test", None);
assert!(result.is_err(), "Should fail when task is denied");
assert_eq!(
result.unwrap_err(),
"Dela task 'test' was denied by the ~/.dela/allowlist.toml"
);
});
}
#[test]
#[serial]
fn test_allow_command_no_task() {
let (project_dir, home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
let result = execute("nonexistent", None);
assert!(result.is_err(), "Should fail when no task found");
assert_eq!(
result.unwrap_err(),
"dela: command or task not found: nonexistent"
);
drop(project_dir);
drop(home_dir);
}
#[test]
#[serial]
fn test_allow_command_with_allow_option() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
assert!(
execute("test", Some(2)).is_ok(),
"Should succeed with allow=2"
);
assert!(
execute("test", Some(3)).is_ok(),
"Should succeed with allow=3"
);
assert!(
execute("test", Some(4)).is_ok(),
"Should succeed with allow=4"
);
let result = execute("test", Some(5));
assert!(result.is_err(), "Should fail with allow=5");
assert_eq!(
result.unwrap_err(),
"Dela task 'test' was denied by the ~/.dela/allowlist.toml"
);
let result = execute("test", Some(1));
assert!(result.is_err(), "Should fail with allow=1");
assert_eq!(
result.unwrap_err(),
"Invalid allow choice 1. Please use a number between 2 and 5."
);
let result = execute("test", Some(6));
assert!(result.is_err(), "Should fail with allow=6");
assert_eq!(
result.unwrap_err(),
"Invalid allow choice 6. Please use a number between 2 and 5."
);
}
#[test]
#[serial]
fn test_allow_command_uninitialized() {
let home_dir = TempDir::new().expect("Failed to create temp HOME directory");
env::set_var("HOME", home_dir.path());
let project_dir = TempDir::new().expect("Failed to create temp directory");
env::set_current_dir(&project_dir).expect("Failed to change directory");
let makefile_content = "
test: ## Running tests
\t@echo Testing...
";
let mut makefile =
File::create(project_dir.path().join("Makefile")).expect("Failed to create Makefile");
makefile
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");
let result = execute("test", None);
assert!(
result.is_err(),
"Should fail when .dela directory doesn't exist"
);
assert_eq!(
result.unwrap_err(),
"Dela is not initialized. Please run 'dela init' first."
);
assert!(
!home_dir.path().join(".dela").exists(),
".dela directory should not be created"
);
}
#[test]
#[serial]
fn test_allow_command_with_allow_option_and_args() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
assert!(
execute("test --verbose --coverage", Some(2)).is_ok(),
"Should succeed with allow=2 and arguments"
);
let result = execute("test --verbose --coverage", Some(5));
assert!(result.is_err(), "Should fail with allow=5 and arguments");
assert_eq!(
result.unwrap_err(),
"Dela task 'test' was denied by the ~/.dela/allowlist.toml"
);
}
#[test]
#[serial]
fn test_allow_command_disambiguated_tasks() {
let (project_dir, _home_dir) = setup_test_env();
env::set_current_dir(&project_dir).expect("Failed to change directory");
let package_json_content = r#"{
"name": "test-package",
"scripts": {
"test": "jest"
}
}"#;
File::create(project_dir.path().join("package.json"))
.unwrap()
.write_all(package_json_content.as_bytes())
.unwrap();
File::create(project_dir.path().join("package-lock.json"))
.unwrap()
.write_all(b"{}")
.unwrap();
let result = execute("test", None);
assert!(result.is_err(), "Should error for ambiguous task");
assert!(
result
.unwrap_err()
.contains("Multiple tasks named 'test' found"),
"Error should mention multiple tasks"
);
let result = execute("test-m", Some(2)); assert!(
result.is_ok(),
"Should succeed with disambiguated task name"
);
let result = execute("test-n", Some(2)); assert!(
result.is_ok(),
"Should succeed with disambiguated task name"
);
let test_with_args = "test-m --verbose --watch";
let result = execute(test_with_args, Some(2)); assert!(
result.is_ok(),
"Should succeed with disambiguated task name and arguments"
);
let npm_test_with_args = "test-n --ci --coverage";
let result = execute(npm_test_with_args, Some(2)); assert!(
result.is_ok(),
"Should succeed with npm disambiguated task name and arguments"
);
}
}