use crate::parsers::{
parse_cmake, parse_docker_compose, parse_github_actions, parse_gradle, parse_makefile,
parse_package_json, parse_pom_xml, parse_pyproject_toml, parse_taskfile, parse_travis_ci,
};
use crate::task_shadowing::check_shadowing;
use crate::types::{Task, TaskDefinitionFile, TaskDefinitionType, TaskFileStatus, TaskRunner};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Default)]
pub struct DiscoveredTaskDefinitions {
pub makefile: Option<TaskDefinitionFile>,
pub package_json: Option<TaskDefinitionFile>,
pub pyproject_toml: Option<TaskDefinitionFile>,
pub taskfile: Option<TaskDefinitionFile>,
pub maven_pom: Option<TaskDefinitionFile>,
pub gradle: Option<TaskDefinitionFile>,
pub github_actions: Option<TaskDefinitionFile>,
pub docker_compose: Option<TaskDefinitionFile>,
pub travis_ci: Option<TaskDefinitionFile>,
pub cmake: Option<TaskDefinitionFile>,
}
#[derive(Debug, Default)]
pub struct DiscoveredTasks {
pub definitions: DiscoveredTaskDefinitions,
pub tasks: Vec<Task>,
pub errors: Vec<String>,
pub task_name_counts: HashMap<String, usize>,
}
impl DiscoveredTasks {
#[cfg(test)]
pub fn new() -> Self {
DiscoveredTasks::default()
}
#[cfg(test)]
pub fn add_task(&mut self, task: Task) {
*self.task_name_counts.entry(task.name.clone()).or_insert(0) += 1;
self.tasks.push(task);
}
}
pub fn discover_tasks(dir: &Path) -> DiscoveredTasks {
let mut discovered = DiscoveredTasks::default();
let _ = discover_makefile_tasks(dir, &mut discovered);
let _ = discover_npm_tasks(dir, &mut discovered);
let _ = discover_python_tasks(dir, &mut discovered);
let _ = discover_taskfile_tasks(dir, &mut discovered);
let _ = discover_maven_tasks(dir, &mut discovered);
let _ = discover_gradle_tasks(dir, &mut discovered);
let _ = discover_github_actions_tasks(dir, &mut discovered);
let _ = discover_docker_compose_tasks(dir, &mut discovered);
let _ = discover_travis_ci_tasks(dir, &mut discovered);
let _ = discover_cmake_tasks(dir, &mut discovered);
discover_shell_script_tasks(dir, &mut discovered);
process_task_disambiguation(&mut discovered);
discovered
}
pub fn process_task_disambiguation(discovered: &mut DiscoveredTasks) {
let mut task_name_counts: HashMap<String, usize> = HashMap::new();
let mut tasks_by_name: HashMap<String, Vec<usize>> = HashMap::new();
for (i, task) in discovered.tasks.iter().enumerate() {
*task_name_counts.entry(task.name.clone()).or_insert(0) += 1;
tasks_by_name
.entry(task.name.clone())
.or_insert_with(Vec::new)
.push(i);
}
discovered.task_name_counts = task_name_counts.clone();
for (name, count) in task_name_counts.iter() {
if *count > 1 {
let task_indices = tasks_by_name.get(name).unwrap();
let mut used_prefixes = std::collections::HashSet::new();
for &idx in task_indices {
let task = &mut discovered.tasks[idx];
let runner_prefix = generate_runner_prefix(&task.runner, &used_prefixes);
used_prefixes.insert(runner_prefix.clone());
task.disambiguated_name = Some(format!("{}-{}", task.name, runner_prefix));
}
}
}
for task in &mut discovered.tasks {
if task.disambiguated_name.is_some() {
continue;
}
if task.shadowed_by.is_some() {
let used_prefixes = std::collections::HashSet::new();
let runner_prefix = generate_runner_prefix(&task.runner, &used_prefixes);
task.disambiguated_name = Some(format!("{}-{}", task.name, runner_prefix));
}
}
}
fn generate_runner_prefix(
runner: &TaskRunner,
used_prefixes: &std::collections::HashSet<String>,
) -> String {
let short_name = runner.short_name().to_lowercase();
let single_char = short_name.chars().next().unwrap().to_string();
if !used_prefixes.contains(&single_char) {
return single_char;
}
let prefix_length = std::cmp::min(3, short_name.len());
let mut prefix = short_name[0..prefix_length].to_string();
if !used_prefixes.contains(&prefix) {
return prefix;
}
for i in (prefix_length + 1)..=short_name.len() {
prefix = short_name[0..i].to_string();
if !used_prefixes.contains(&prefix) {
return prefix;
}
}
let mut i = 1;
loop {
let numbered_prefix = format!("{}{}", short_name, i);
if !used_prefixes.contains(&numbered_prefix) {
return numbered_prefix;
}
i += 1;
}
}
pub fn is_task_ambiguous(discovered: &DiscoveredTasks, task_name: &str) -> bool {
discovered
.task_name_counts
.get(task_name)
.map_or(false, |&count| count > 1)
}
#[allow(dead_code)]
pub fn get_disambiguated_task_names(discovered: &DiscoveredTasks, task_name: &str) -> Vec<String> {
discovered
.tasks
.iter()
.filter(|t| t.name == task_name)
.filter_map(|t| t.disambiguated_name.clone())
.collect()
}
pub fn get_matching_tasks<'a>(discovered: &'a DiscoveredTasks, task_name: &str) -> Vec<&'a Task> {
let mut result = Vec::new();
if let Some(task) = discovered.tasks.iter().find(|t| {
t.disambiguated_name
.as_ref()
.map_or(false, |dn| dn == task_name)
}) {
result.push(task);
return result;
}
result.extend(discovered.tasks.iter().filter(|t| t.name == task_name));
result
}
pub fn format_ambiguous_task_error(task_name: &str, matching_tasks: &[&Task]) -> String {
let mut msg = format!("Multiple tasks named '{}' found. Use one of:\n", task_name);
for task in matching_tasks {
if let Some(disambiguated) = &task.disambiguated_name {
msg.push_str(&format!(
" • {} ({} from {})\n",
disambiguated,
task.runner.short_name(),
task.file_path.display()
));
}
}
msg.push_str("Please use the specific task name with its suffix to disambiguate.");
msg
}
fn set_definition(discovered: &mut DiscoveredTasks, definition: TaskDefinitionFile) {
match definition.definition_type {
TaskDefinitionType::Makefile => discovered.definitions.makefile = Some(definition),
TaskDefinitionType::PackageJson => discovered.definitions.package_json = Some(definition),
TaskDefinitionType::PyprojectToml => {
discovered.definitions.pyproject_toml = Some(definition)
}
TaskDefinitionType::Taskfile => discovered.definitions.taskfile = Some(definition),
TaskDefinitionType::MavenPom => discovered.definitions.maven_pom = Some(definition),
TaskDefinitionType::Gradle => discovered.definitions.gradle = Some(definition),
TaskDefinitionType::GitHubActions => {
discovered.definitions.github_actions = Some(definition)
}
TaskDefinitionType::DockerCompose => {
discovered.definitions.docker_compose = Some(definition)
}
TaskDefinitionType::TravisCi => discovered.definitions.travis_ci = Some(definition),
TaskDefinitionType::CMake => discovered.definitions.cmake = Some(definition),
_ => {}
}
}
fn handle_discovery_error(
error: String,
file_path: PathBuf,
definition_type: TaskDefinitionType,
discovered: &mut DiscoveredTasks,
) {
discovered.errors.push(format!(
"Failed to parse {}: {}",
file_path.display(),
error
));
let definition = TaskDefinitionFile {
path: file_path,
definition_type,
status: TaskFileStatus::ParseError(error),
};
set_definition(discovered, definition);
}
fn handle_discovery_success(
mut tasks: Vec<Task>,
file_path: PathBuf,
definition_type: TaskDefinitionType,
discovered: &mut DiscoveredTasks,
) {
for task in &mut tasks {
task.shadowed_by = check_shadowing(&task.name);
}
let definition = TaskDefinitionFile {
path: file_path,
definition_type,
status: TaskFileStatus::Parsed,
};
set_definition(discovered, definition);
discovered.tasks.extend(tasks);
}
fn discover_makefile_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let makefile_path = dir.join("Makefile");
if !makefile_path.exists() {
discovered.definitions.makefile = Some(TaskDefinitionFile {
path: makefile_path.clone(),
definition_type: TaskDefinitionType::Makefile,
status: TaskFileStatus::NotFound,
});
return Ok(());
}
match parse_makefile::parse(&makefile_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
makefile_path,
TaskDefinitionType::Makefile,
discovered,
);
}
Err(e) => {
handle_discovery_error(e, makefile_path, TaskDefinitionType::Makefile, discovered);
}
}
Ok(())
}
fn discover_npm_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let package_json = dir.join("package.json");
if !package_json.exists() {
discovered.definitions.package_json = Some(TaskDefinitionFile {
path: package_json.clone(),
definition_type: TaskDefinitionType::PackageJson,
status: TaskFileStatus::NotFound,
});
return Ok(());
}
match parse_package_json::parse(&package_json) {
Ok(tasks) => {
handle_discovery_success(
tasks,
package_json,
TaskDefinitionType::PackageJson,
discovered,
);
}
Err(e) => {
handle_discovery_error(e, package_json, TaskDefinitionType::PackageJson, discovered);
}
}
Ok(())
}
fn discover_python_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let pyproject_toml = dir.join("pyproject.toml");
if !pyproject_toml.exists() {
discovered.definitions.pyproject_toml = Some(TaskDefinitionFile {
path: pyproject_toml.clone(),
definition_type: TaskDefinitionType::PyprojectToml,
status: TaskFileStatus::NotFound,
});
return Ok(());
}
match parse_pyproject_toml::parse(&pyproject_toml) {
Ok(tasks) => {
handle_discovery_success(
tasks,
pyproject_toml,
TaskDefinitionType::PyprojectToml,
discovered,
);
}
Err(e) => {
handle_discovery_error(
e,
pyproject_toml,
TaskDefinitionType::PyprojectToml,
discovered,
);
}
}
Ok(())
}
fn discover_taskfile_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let possible_taskfiles = [
"Taskfile.yml",
"taskfile.yml",
"Taskfile.yaml",
"taskfile.yaml",
"Taskfile.dist.yml",
"taskfile.dist.yml",
"Taskfile.dist.yaml",
"taskfile.dist.yaml",
];
let mut taskfile_path = None;
for filename in &possible_taskfiles {
let path = dir.join(filename);
if path.exists() {
taskfile_path = Some(path);
break;
}
}
let default_path = dir.join("Taskfile.yml");
if let Some(taskfile_path) = taskfile_path {
let mut definition = TaskDefinitionFile {
path: taskfile_path.clone(),
definition_type: TaskDefinitionType::Taskfile,
status: TaskFileStatus::NotImplemented,
};
match parse_taskfile::parse(&taskfile_path) {
Ok(tasks) => {
definition.status = TaskFileStatus::Parsed;
discovered.tasks.extend(tasks);
}
Err(e) => {
definition.status = TaskFileStatus::ParseError(e.clone());
discovered
.errors
.push(format!("Error parsing {}: {}", taskfile_path.display(), e));
}
}
set_definition(discovered, definition);
} else {
discovered.definitions.taskfile = Some(TaskDefinitionFile {
path: default_path,
definition_type: TaskDefinitionType::Taskfile,
status: TaskFileStatus::NotFound,
});
}
Ok(())
}
fn discover_maven_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let pom_path = dir.join("pom.xml");
if !pom_path.exists() {
return Ok(());
}
match parse_pom_xml(&pom_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
pom_path.clone(),
TaskDefinitionType::MavenPom,
discovered,
);
Ok(())
}
Err(e) => {
handle_discovery_error(e, pom_path, TaskDefinitionType::MavenPom, discovered);
Err("Error parsing pom.xml".to_string())
}
}
}
fn discover_gradle_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let build_gradle_path = dir.join("build.gradle");
if build_gradle_path.exists() {
match parse_gradle::parse(&build_gradle_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
build_gradle_path.clone(),
TaskDefinitionType::Gradle,
discovered,
);
return Ok(());
}
Err(e) => {
handle_discovery_error(
e,
build_gradle_path,
TaskDefinitionType::Gradle,
discovered,
);
return Err("Error parsing build.gradle".to_string());
}
}
}
let build_gradle_kts_path = dir.join("build.gradle.kts");
if build_gradle_kts_path.exists() {
match parse_gradle::parse(&build_gradle_kts_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
build_gradle_kts_path.clone(),
TaskDefinitionType::Gradle,
discovered,
);
Ok(())
}
Err(e) => {
handle_discovery_error(
e,
build_gradle_kts_path,
TaskDefinitionType::Gradle,
discovered,
);
Err("Error parsing build.gradle.kts".to_string())
}
}
} else {
discovered.definitions.gradle = Some(TaskDefinitionFile {
path: build_gradle_path,
definition_type: TaskDefinitionType::Gradle,
status: TaskFileStatus::NotFound,
});
Ok(())
}
}
fn discover_github_actions_tasks(
dir: &Path,
discovered: &mut DiscoveredTasks,
) -> Result<(), String> {
let mut workflow_files = Vec::new();
let workflows_dir = dir.join(".github").join("workflows");
if workflows_dir.exists() && workflows_dir.is_dir() {
match fs::read_dir(&workflows_dir) {
Ok(entries) => {
let files: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
if let Some(ext) = path.extension() {
ext == "yml" || ext == "yaml"
} else {
false
}
})
.collect();
workflow_files.extend(files);
}
Err(e) => {
discovered
.errors
.push(format!("Failed to read .github/workflows directory: {}", e));
}
}
}
for filename in &[
"workflow.yml",
"workflow.yaml",
".github/workflow.yml",
".github/workflow.yaml",
] {
let file_path = dir.join(filename);
if file_path.exists() && file_path.is_file() {
workflow_files.push(file_path);
}
}
for custom_dir in &["workflows", "custom/workflows", ".gitlab/workflows"] {
let custom_path = dir.join(custom_dir);
if custom_path.exists() && custom_path.is_dir() {
if let Ok(entries) = fs::read_dir(&custom_path) {
let files: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
if let Some(ext) = path.extension() {
ext == "yml" || ext == "yaml"
} else {
false
}
})
.collect();
workflow_files.extend(files);
}
}
}
if workflow_files.is_empty() {
return Ok(());
}
let mut all_tasks = Vec::new();
let mut errors = Vec::new();
let workflows_parent = dir.join(".github").join("workflows");
for file_path in workflow_files {
match parse_github_actions(&file_path) {
Ok(mut tasks) => {
for task in &mut tasks {
task.file_path = workflows_parent.clone();
}
all_tasks.extend(tasks);
}
Err(e) => errors.push(format!(
"Failed to parse workflow file {:?}: {}",
file_path, e
)),
}
}
if !errors.is_empty() {
discovered.errors.extend(errors);
}
if !all_tasks.is_empty() {
discovered.definitions.github_actions = Some(TaskDefinitionFile {
path: workflows_parent,
definition_type: TaskDefinitionType::GitHubActions,
status: TaskFileStatus::Parsed,
});
discovered.tasks.extend(all_tasks);
}
Ok(())
}
fn discover_docker_compose_tasks(
dir: &Path,
discovered: &mut DiscoveredTasks,
) -> Result<(), String> {
let docker_compose_files = parse_docker_compose::find_docker_compose_files(dir);
if docker_compose_files.is_empty() {
let default_path = dir.join("docker-compose.yml");
discovered.definitions.docker_compose = Some(TaskDefinitionFile {
path: default_path,
definition_type: TaskDefinitionType::DockerCompose,
status: TaskFileStatus::NotFound,
});
return Ok(());
}
let docker_compose_path = &docker_compose_files[0];
match parse_docker_compose::parse(docker_compose_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
docker_compose_path.clone(),
TaskDefinitionType::DockerCompose,
discovered,
);
}
Err(e) => {
handle_discovery_error(
e,
docker_compose_path.clone(),
TaskDefinitionType::DockerCompose,
discovered,
);
}
}
Ok(())
}
fn discover_travis_ci_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let travis_ci_path = dir.join(".travis.yml");
if travis_ci_path.exists() {
match parse_travis_ci(&travis_ci_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
travis_ci_path.clone(),
TaskDefinitionType::TravisCi,
discovered,
);
}
Err(error) => {
handle_discovery_error(
error,
travis_ci_path.clone(),
TaskDefinitionType::TravisCi,
discovered,
);
}
}
} else {
set_definition(
discovered,
TaskDefinitionFile {
path: travis_ci_path,
definition_type: TaskDefinitionType::TravisCi,
status: TaskFileStatus::NotFound,
},
);
}
Ok(())
}
fn discover_cmake_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> Result<(), String> {
let cmake_path = dir.join("CMakeLists.txt");
if !cmake_path.exists() {
return Ok(());
}
match parse_cmake::parse(&cmake_path) {
Ok(tasks) => {
handle_discovery_success(
tasks,
cmake_path.clone(),
TaskDefinitionType::CMake,
discovered,
);
Ok(())
}
Err(e) => {
handle_discovery_error(e, cmake_path, TaskDefinitionType::CMake, discovered);
Err("Error parsing CMakeLists.txt".to_string())
}
}
}
fn discover_shell_script_tasks(dir: &Path, discovered: &mut DiscoveredTasks) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
if extension == "sh" {
let name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
discovered.tasks.push(Task {
name: name.clone(),
file_path: path,
definition_type: TaskDefinitionType::ShellScript,
runner: TaskRunner::ShellScript,
source_name: name.clone(),
description: None,
shadowed_by: check_shadowing(&name),
disambiguated_name: None,
});
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::environment::{reset_to_real_environment, set_test_environment, TestEnvironment};
use crate::task_shadowing::{enable_mock, mock_executable, reset_mock};
use crate::types::ShadowType;
use serial_test::serial;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
struct MockTaskExecutor {
execute_fn: Box<dyn FnMut(&Task) -> Result<(), String>>,
}
impl MockTaskExecutor {
fn new() -> Self {
MockTaskExecutor {
execute_fn: Box::new(|_| Ok(())),
}
}
fn expect_execute(&mut self) -> &mut MockTaskExecutor {
self
}
fn times(&mut self, _: usize) -> &mut MockTaskExecutor {
self
}
fn returning<F>(&mut self, f: F) -> &mut MockTaskExecutor
where
F: FnMut(&Task) -> Result<(), String> + 'static,
{
self.execute_fn = Box::new(f);
self
}
fn execute(&mut self, task: &Task) -> Result<(), String> {
(self.execute_fn)(task)
}
}
struct CommandExecutor {
executor: MockTaskExecutor,
}
impl CommandExecutor {
fn new(executor: MockTaskExecutor) -> Self {
CommandExecutor { executor }
}
fn execute_task_by_name(
&mut self,
discovered_tasks: &mut DiscoveredTasks,
task_name: &str,
_args: &[&str],
) -> Result<(), String> {
let matching_tasks = get_matching_tasks(discovered_tasks, task_name);
if matching_tasks.is_empty() {
return Err(format!("dela: command or task not found: {}", task_name));
}
if matching_tasks.len() > 1 {
let error_msg = format_ambiguous_task_error(task_name, &matching_tasks);
return Err(format!(
"Ambiguous task name: '{}'. {}",
task_name, error_msg
));
}
if task_name == "test" && is_task_ambiguous(discovered_tasks, task_name) {
return Err(format!("Ambiguous task name: '{}'", task_name));
}
self.executor.execute(matching_tasks[0])
}
}
fn create_test_makefile(dir: &Path, content: &str) {
let mut file = File::create(dir.join("Makefile")).unwrap();
writeln!(file, "{}", content).unwrap();
}
#[test]
fn test_discover_tasks_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let discovered = discover_tasks(temp_dir.path());
assert!(discovered.tasks.is_empty());
assert!(discovered.errors.is_empty());
assert!(matches!(
discovered.definitions.makefile.unwrap().status,
TaskFileStatus::NotFound
));
assert!(matches!(
discovered.definitions.package_json.unwrap().status,
TaskFileStatus::NotFound
));
assert!(matches!(
discovered.definitions.pyproject_toml.unwrap().status,
TaskFileStatus::NotFound
));
}
#[test]
fn test_discover_tasks_with_makefile() {
let temp_dir = TempDir::new().unwrap();
let content = r#".PHONY: build test
build:
@echo "Building the project"
cargo build
test:
@echo "Running tests"
cargo test"#;
create_test_makefile(temp_dir.path(), content);
let discovered = discover_tasks(temp_dir.path());
assert_eq!(discovered.tasks.len(), 2);
assert!(discovered.errors.is_empty());
assert!(matches!(
discovered.definitions.makefile.unwrap().status,
TaskFileStatus::Parsed
));
let build_task = discovered.tasks.iter().find(|t| t.name == "build").unwrap();
assert_eq!(build_task.runner, TaskRunner::Make);
assert_eq!(
build_task.description,
Some("Building the project".to_string())
);
let test_task = discovered.tasks.iter().find(|t| t.name == "test").unwrap();
assert_eq!(test_task.runner, TaskRunner::Make);
assert_eq!(test_task.description, Some("Running tests".to_string()));
}
#[test]
fn test_discover_tasks_with_invalid_makefile() {
let temp_dir = TempDir::new().unwrap();
let content = "<hello>not a make file</hello>";
create_test_makefile(temp_dir.path(), content);
let discovered = discover_tasks(temp_dir.path());
assert!(
discovered.tasks.is_empty(),
"Expected no tasks, found: {:?}",
discovered.tasks
);
assert!(matches!(
discovered.definitions.makefile.unwrap().status,
TaskFileStatus::ParseError(_)
));
}
#[test]
fn test_discover_tasks_with_unimplemented_parsers() {
let temp_dir = TempDir::new().unwrap();
let mut file = File::create(temp_dir.path().join("pyproject.toml")).unwrap();
write!(file, "invalid toml content").unwrap();
let discovered = discover_tasks(temp_dir.path());
assert!(matches!(
discovered.definitions.pyproject_toml.unwrap().status,
TaskFileStatus::ParseError(_)
));
}
#[test]
fn test_discover_npm_tasks() {
let temp_dir = TempDir::new().unwrap();
let content = r#"{
"name": "test-package",
"scripts": {
"test": "jest",
"build": "tsc"
}
}"#;
let mut file = File::create(temp_dir.path().join("package.json")).unwrap();
write!(file, "{}", content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let package_json_def = discovered.definitions.package_json.unwrap();
assert_eq!(package_json_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 2);
let test_task = discovered.tasks.iter().find(|t| t.name == "test").unwrap();
assert!(matches!(
test_task.runner,
TaskRunner::NodeNpm | TaskRunner::NodeYarn | TaskRunner::NodePnpm | TaskRunner::NodeBun
));
assert_eq!(test_task.description, Some("jest".to_string()));
let build_task = discovered.tasks.iter().find(|t| t.name == "build").unwrap();
assert!(matches!(
build_task.runner,
TaskRunner::NodeNpm | TaskRunner::NodeYarn | TaskRunner::NodePnpm | TaskRunner::NodeBun
));
assert_eq!(build_task.description, Some("tsc".to_string()));
}
#[test]
fn test_discover_npm_tasks_invalid_json() {
let temp_dir = TempDir::new().unwrap();
let content = r#"{ invalid json }"#;
let mut file = File::create(temp_dir.path().join("package.json")).unwrap();
write!(file, "{}", content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let package_json_def = discovered.definitions.package_json.unwrap();
assert!(matches!(
package_json_def.status,
TaskFileStatus::ParseError(_)
));
assert!(discovered.tasks.is_empty());
}
#[test]
fn test_discover_python_tasks() {
let temp_dir = TempDir::new().unwrap();
reset_mock();
enable_mock();
mock_executable("uv");
let content = r#"
[project]
name = "test-project"
[project.scripts]
serve = "uvicorn main:app --reload"
"#;
let pyproject_path = temp_dir.path().join("pyproject.toml");
let mut file = File::create(&pyproject_path).unwrap();
write!(file, "{}", content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let pyproject_def = discovered.definitions.pyproject_toml.unwrap();
assert_eq!(pyproject_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 1);
let serve_task = discovered.tasks.iter().find(|t| t.name == "serve").unwrap();
assert_eq!(serve_task.runner, TaskRunner::PythonUv);
assert_eq!(
serve_task.description,
Some("python script: uvicorn main:app --reload".to_string())
);
reset_mock();
}
#[test]
fn test_discover_python_poetry_tasks() {
let temp_dir = TempDir::new().unwrap();
reset_mock();
enable_mock();
mock_executable("poetry");
File::create(temp_dir.path().join("poetry.lock")).unwrap();
let content = r#"
[tool.poetry]
name = "test-project"
[tool.poetry.scripts]
serve = "python -m http.server"
test = "pytest"
lint = "flake8"
"#;
let pyproject_path = temp_dir.path().join("pyproject.toml");
let mut file = File::create(&pyproject_path).unwrap();
write!(file, "{}", content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let pyproject_def = discovered.definitions.pyproject_toml.unwrap();
assert_eq!(pyproject_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 3);
for task in &discovered.tasks {
assert_eq!(task.runner, TaskRunner::PythonPoetry);
}
let serve_task = discovered.tasks.iter().find(|t| t.name == "serve").unwrap();
assert_eq!(
serve_task.description,
Some("python script: python -m http.server".to_string())
);
let test_task = discovered.tasks.iter().find(|t| t.name == "test").unwrap();
assert_eq!(
test_task.description,
Some("python script: pytest".to_string())
);
let lint_task = discovered.tasks.iter().find(|t| t.name == "lint").unwrap();
assert_eq!(
lint_task.description,
Some("python script: flake8".to_string())
);
reset_mock();
}
#[test]
fn test_discover_tasks_multiple_files() {
let temp_dir = TempDir::new().unwrap();
reset_mock();
enable_mock();
mock_executable("npm");
mock_executable("poetry");
let makefile_content = r#".PHONY: build test
build:
@echo "Building the project"
test:
@echo "Running tests""#;
create_test_makefile(temp_dir.path(), makefile_content);
let package_json_content = r#"{
"name": "test-package",
"scripts": {
"start": "node index.js",
"lint": "eslint ."
}
}"#;
let mut package_json = File::create(temp_dir.path().join("package.json")).unwrap();
write!(package_json, "{}", package_json_content).unwrap();
let pyproject_content = r#"
[tool.poetry]
name = "test-project"
[tool.poetry.scripts]
serve = "python -m http.server"
"#;
let mut pyproject = File::create(temp_dir.path().join("pyproject.toml")).unwrap();
write!(pyproject, "{}", pyproject_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
assert!(matches!(
discovered.definitions.makefile.unwrap().status,
TaskFileStatus::Parsed
));
assert!(matches!(
discovered.definitions.package_json.unwrap().status,
TaskFileStatus::Parsed
));
assert!(matches!(
discovered.definitions.pyproject_toml.unwrap().status,
TaskFileStatus::Parsed
));
assert_eq!(discovered.tasks.len(), 5);
let make_tasks: Vec<_> = discovered
.tasks
.iter()
.filter(|t| matches!(t.runner, TaskRunner::Make))
.collect();
assert_eq!(make_tasks.len(), 2);
let node_tasks: Vec<_> = discovered
.tasks
.iter()
.filter(|t| {
matches!(
t.runner,
TaskRunner::NodeNpm
| TaskRunner::NodeYarn
| TaskRunner::NodePnpm
| TaskRunner::NodeBun
)
})
.collect();
assert_eq!(node_tasks.len(), 2);
let python_tasks: Vec<_> = discovered
.tasks
.iter()
.filter(|t| matches!(t.runner, TaskRunner::PythonPoetry))
.collect();
assert_eq!(python_tasks.len(), 1);
reset_mock();
}
#[test]
fn test_discover_tasks_with_name_collision() {
let temp_dir = TempDir::new().unwrap();
let makefile_content = r#".PHONY: test cd
test:
@echo "Running tests"
cd:
@echo "Change directory"
"#;
create_test_makefile(temp_dir.path(), makefile_content);
let package_json_path = temp_dir.path().join("package.json");
std::fs::write(
&package_json_path,
r#"{
"name": "test-package",
"scripts": {
"test": "jest"
}
}"#,
)
.unwrap();
let discovered = discover_tasks(temp_dir.path());
assert!(discovered.tasks.len() >= 2);
let make_test = discovered
.tasks
.iter()
.find(|t| matches!(t.runner, TaskRunner::Make) && t.name == "test")
.unwrap();
assert!(make_test.description.as_ref().unwrap().contains("Running"));
let node_test = discovered
.tasks
.iter()
.find(|t| {
matches!(
t.runner,
TaskRunner::NodeNpm
| TaskRunner::NodeYarn
| TaskRunner::NodePnpm
| TaskRunner::NodeBun
) && t.name == "test"
})
.unwrap();
assert_eq!(node_test.description, Some("jest".to_string()));
}
#[test]
#[serial]
fn test_discover_tasks_with_shadowing() {
let temp_dir = TempDir::new().unwrap();
let makefile_path = temp_dir.path().join("Makefile");
let env = TestEnvironment::new().with_shell("/bin/zsh");
set_test_environment(env);
let content = ".PHONY: test cd\n\ntest:\n\t@echo \"Running tests\"\ncd:\n\t@echo \"Change directory\"\n";
File::create(&makefile_path)
.unwrap()
.write_all(content.as_bytes())
.unwrap();
let discovered = discover_tasks(temp_dir.path());
let cd_task = discovered
.tasks
.iter()
.find(|t| t.name == "cd")
.expect("cd task not found");
assert!(matches!(
cd_task.shadowed_by,
Some(ShadowType::ShellBuiltin(_))
));
assert_eq!(cd_task.disambiguated_name, Some("cd-m".to_string()));
let test_task = discovered
.tasks
.iter()
.find(|t| t.name == "test")
.expect("test task not found");
assert!(matches!(
test_task.shadowed_by,
Some(ShadowType::ShellBuiltin(_))
));
assert_eq!(test_task.disambiguated_name, Some("test-m".to_string()));
reset_to_real_environment();
}
#[test]
#[serial]
fn test_parse_package_json() {
let temp_dir = TempDir::new().unwrap();
let package_json_path = temp_dir.path().join("package.json");
let content = r#"{
"name": "test-package",
"scripts": {
"test": "jest",
"build": "tsc"
}
}"#;
File::create(&package_json_path)
.unwrap()
.write_all(content.as_bytes())
.unwrap();
let tasks = parse_package_json::parse(&package_json_path).unwrap();
assert_eq!(tasks.len(), 2);
let test_task = tasks.iter().find(|t| t.name == "test").unwrap();
assert!(matches!(
test_task.runner,
TaskRunner::NodeNpm | TaskRunner::NodeYarn | TaskRunner::NodePnpm | TaskRunner::NodeBun
));
assert_eq!(test_task.description, Some("jest".to_string()));
let build_task = tasks.iter().find(|t| t.name == "build").unwrap();
assert!(matches!(
build_task.runner,
TaskRunner::NodeNpm | TaskRunner::NodeYarn | TaskRunner::NodePnpm | TaskRunner::NodeBun
));
assert_eq!(build_task.description, Some("tsc".to_string()));
}
#[test]
fn test_discover_taskfile_tasks() {
let temp_dir = TempDir::new().unwrap();
reset_mock();
enable_mock();
mock_executable("task");
let content = r#"version: '3'
tasks:
test:
desc: Test task
cmds:
- echo "Running tests"
build:
desc: Build task
cmds:
- echo "Building project"
deps:
desc: Task with dependencies
deps:
- test
cmds:
- echo "Running dependent task""#;
let taskfile_path = temp_dir.path().join("Taskfile.yml");
let mut file = File::create(&taskfile_path).unwrap();
write!(file, "{}", content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let taskfile_def = discovered.definitions.taskfile.unwrap();
assert_eq!(taskfile_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 3);
let test_task = discovered.tasks.iter().find(|t| t.name == "test").unwrap();
assert_eq!(test_task.runner, TaskRunner::Task);
assert_eq!(test_task.description, Some("Test task".to_string()));
let build_task = discovered.tasks.iter().find(|t| t.name == "build").unwrap();
assert_eq!(build_task.runner, TaskRunner::Task);
assert_eq!(build_task.description, Some("Build task".to_string()));
let deps_task = discovered.tasks.iter().find(|t| t.name == "deps").unwrap();
assert_eq!(deps_task.runner, TaskRunner::Task);
assert_eq!(
deps_task.description,
Some("Task with dependencies".to_string())
);
reset_mock();
}
#[test]
fn test_discover_maven_tasks() {
let temp_dir = tempfile::tempdir().unwrap();
let dir_path = temp_dir.path();
let pom_xml_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sample-project</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<executions>
<execution>
<id>compile-java</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.0</version>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
</project>"#;
std::fs::write(dir_path.join("pom.xml"), pom_xml_content).unwrap();
let discovered = discover_tasks(dir_path);
assert!(discovered.definitions.maven_pom.is_some());
assert_eq!(
discovered.definitions.maven_pom.unwrap().status,
TaskFileStatus::Parsed
);
assert!(discovered.tasks.iter().any(|t| t.name == "clean"));
assert!(discovered.tasks.iter().any(|t| t.name == "compile"));
assert!(discovered.tasks.iter().any(|t| t.name == "test"));
assert!(discovered.tasks.iter().any(|t| t.name == "package"));
assert!(discovered.tasks.iter().any(|t| t.name == "install"));
assert!(discovered.tasks.iter().any(|t| t.name == "profile:dev"));
assert!(discovered.tasks.iter().any(|t| t.name == "profile:prod"));
assert!(discovered
.tasks
.iter()
.any(|t| t.name == "maven-compiler-plugin:compile"));
assert!(discovered
.tasks
.iter()
.any(|t| t.name == "spring-boot-maven-plugin:build-info"));
for task in discovered.tasks {
if task.definition_type == TaskDefinitionType::MavenPom {
assert_eq!(task.runner, TaskRunner::Maven);
}
}
}
#[test]
#[serial_test::serial]
fn test_discover_tasks_with_missing_runners() {
reset_mock();
enable_mock();
let temp_dir = TempDir::new().unwrap();
let pom_content = r#"<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
</project>"#;
let pom_path = temp_dir.path().join("pom.xml");
let mut pom_file = File::create(&pom_path).unwrap();
pom_file.write_all(pom_content.as_bytes()).unwrap();
let gradle_content = "task gradleTest { description 'Test task' }";
let gradle_path = temp_dir.path().join("build.gradle");
let mut gradle_file = File::create(&gradle_path).unwrap();
gradle_file.write_all(gradle_content.as_bytes()).unwrap();
let env = TestEnvironment::new();
set_test_environment(env);
let discovered = discover_tasks(temp_dir.path());
assert!(
discovered
.tasks
.iter()
.any(|t| t.runner == TaskRunner::Maven),
"Maven tasks should be discovered even if runner is unavailable"
);
assert!(
discovered
.tasks
.iter()
.any(|t| t.runner == TaskRunner::Gradle),
"Gradle tasks should be discovered even if runner is unavailable"
);
for task in &discovered.tasks {
if task.runner == TaskRunner::Maven || task.runner == TaskRunner::Gradle {
assert!(
!crate::runner::is_runner_available(&task.runner),
"Runner for {} should be marked as unavailable",
task.name
);
}
}
reset_mock();
reset_to_real_environment();
}
#[test]
fn test_discover_github_actions_tasks_in_different_locations() {
let temp_dir = TempDir::new().unwrap();
let github_workflows_dir = temp_dir.path().join(".github").join("workflows");
std::fs::create_dir_all(&github_workflows_dir).unwrap();
let github_workflow_content = r#"
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: echo "Building..."
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Test
run: echo "Testing..."
"#;
std::fs::write(github_workflows_dir.join("ci.yml"), github_workflow_content).unwrap();
let root_workflow_content = r#"
name: Root Workflow
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy
run: echo "Deploying..."
"#;
std::fs::write(temp_dir.path().join("workflow.yml"), root_workflow_content).unwrap();
let custom_dir = temp_dir.path().join("custom").join("workflows");
std::fs::create_dir_all(&custom_dir).unwrap();
let custom_workflow_content = r#"
name: Custom Workflow
on: [workflow_dispatch]
jobs:
custom:
runs-on: ubuntu-latest
steps:
- name: Custom Action
run: echo "Custom action..."
"#;
std::fs::write(custom_dir.join("custom.yml"), custom_workflow_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
assert!(matches!(
discovered.definitions.github_actions.unwrap().status,
TaskFileStatus::Parsed
));
let act_tasks: Vec<&Task> = discovered
.tasks
.iter()
.filter(|t| t.runner == TaskRunner::Act)
.collect();
assert_eq!(
act_tasks.len(),
3,
"Should discover 3 GitHub Actions workflows"
);
let workflow_names: Vec<&str> = act_tasks.iter().map(|t| t.name.as_str()).collect();
assert!(workflow_names.contains(&"ci"));
assert!(workflow_names.contains(&"workflow"));
assert!(workflow_names.contains(&"custom"));
let common_path = temp_dir.path().join(".github").join("workflows");
for task in act_tasks {
assert_eq!(task.file_path, common_path);
}
}
#[test]
#[serial]
fn test_process_disambiguation_for_shadowed_tasks() {
let mut discovered = DiscoveredTasks::default();
discovered.tasks.push(Task {
name: "test".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "test".to_string(),
description: None,
shadowed_by: Some(ShadowType::ShellBuiltin("bash".to_string())),
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "ls".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "ls".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/bin/ls".to_string())),
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "build".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "build".to_string(),
description: None,
shadowed_by: None,
disambiguated_name: None,
});
process_task_disambiguation(&mut discovered);
assert_eq!(
discovered.tasks[0].disambiguated_name,
Some("test-m".to_string())
);
assert_eq!(
discovered.tasks[1].disambiguated_name,
Some("ls-m".to_string())
);
assert_eq!(discovered.tasks[2].disambiguated_name, None);
}
#[test]
#[serial]
fn test_process_disambiguation_mixed_scenarios() {
let mut discovered = DiscoveredTasks::default();
discovered.tasks.push(Task {
name: "test".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "test".to_string(),
description: None,
shadowed_by: None,
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "test".to_string(),
file_path: PathBuf::from("/test/package.json"),
definition_type: TaskDefinitionType::PackageJson,
runner: TaskRunner::NodeNpm,
source_name: "test".to_string(),
description: None,
shadowed_by: None,
disambiguated_name: Some("test-npm".to_string()),
});
discovered.tasks.push(Task {
name: "ls".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "ls".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/bin/ls".to_string())),
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "cd".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "cd".to_string(),
description: None,
shadowed_by: Some(ShadowType::ShellBuiltin("bash".to_string())),
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "cd".to_string(),
file_path: PathBuf::from("/test/Taskfile.yml"),
definition_type: TaskDefinitionType::Taskfile,
runner: TaskRunner::Task,
source_name: "cd".to_string(),
description: None,
shadowed_by: Some(ShadowType::ShellBuiltin("bash".to_string())),
disambiguated_name: None,
});
discovered.tasks.push(Task {
name: "build".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "build".to_string(),
description: None,
shadowed_by: None,
disambiguated_name: None,
});
process_task_disambiguation(&mut discovered);
let test_tasks: Vec<_> = discovered
.tasks
.iter()
.filter(|t| t.name == "test")
.collect();
assert_eq!(test_tasks.len(), 2);
assert!(test_tasks[0].disambiguated_name.is_some());
assert!(test_tasks[1].disambiguated_name.is_some());
assert_ne!(
test_tasks[0].disambiguated_name,
test_tasks[1].disambiguated_name
);
let ls_task = discovered
.tasks
.iter()
.find(|t| t.name == "ls")
.expect("ls task not found");
assert_eq!(ls_task.disambiguated_name, Some("ls-m".to_string()));
let cd_tasks: Vec<_> = discovered.tasks.iter().filter(|t| t.name == "cd").collect();
assert_eq!(cd_tasks.len(), 2);
assert!(cd_tasks[0].disambiguated_name.is_some());
assert!(cd_tasks[1].disambiguated_name.is_some());
assert_ne!(
cd_tasks[0].disambiguated_name,
cd_tasks[1].disambiguated_name
);
let cd_disambiguated_names: Vec<_> = cd_tasks
.iter()
.filter_map(|t| t.disambiguated_name.as_ref())
.map(|s| s.as_str())
.collect();
assert!(cd_disambiguated_names.contains(&"cd-m"));
assert!(cd_disambiguated_names.contains(&"cd-t"));
let build_task = discovered
.tasks
.iter()
.find(|t| t.name == "build")
.expect("build task not found");
assert_eq!(build_task.disambiguated_name, None);
}
#[test]
#[serial]
fn test_get_matching_tasks_with_shadowed_task() {
let mut discovered = DiscoveredTasks::default();
discovered.tasks.push(Task {
name: "install".to_string(),
file_path: PathBuf::from("/test/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "install".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/usr/bin/install".to_string())),
disambiguated_name: Some("install-m".to_string()),
});
let matching_by_original = get_matching_tasks(&discovered, "install");
assert_eq!(matching_by_original.len(), 1);
let matching_by_disambiguated = get_matching_tasks(&discovered, "install-m");
assert_eq!(matching_by_disambiguated.len(), 1);
assert_eq!(matching_by_original[0].name, "install");
assert_eq!(matching_by_disambiguated[0].name, "install");
assert_eq!(
matching_by_disambiguated[0].disambiguated_name,
Some("install-m".to_string())
);
}
#[test]
fn test_execute_task_with_disambiguated_name() {
let mut discovered_tasks = DiscoveredTasks::new();
let task = Task {
name: "test".to_string(),
file_path: PathBuf::from("/path/to/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "test".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/bin/test".to_string())),
disambiguated_name: Some("test-m".to_string()),
};
discovered_tasks.add_task(task);
let mut mock_executor = MockTaskExecutor::new();
mock_executor.expect_execute().times(1).returning(|task| {
assert_eq!(task.name, "test"); assert_eq!(task.disambiguated_name, Some("test-m".to_string())); assert!(task.shadowed_by.is_some()); Ok(())
});
let mut executor = CommandExecutor::new(mock_executor);
let result = executor.execute_task_by_name(&mut discovered_tasks, "test-m", &[]);
assert!(result.is_ok());
}
#[test]
fn test_execute_task_by_either_name() {
let mut discovered_tasks = DiscoveredTasks::new();
let task = Task {
name: "grep".to_string(),
file_path: PathBuf::from("/path/to/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "grep".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/bin/grep".to_string())),
disambiguated_name: Some("grep-m".to_string()),
};
discovered_tasks.add_task(task);
let mut mock_executor = MockTaskExecutor::new();
mock_executor.expect_execute().times(2).returning(|task| {
assert_eq!(task.name, "grep"); Ok(())
});
let mut executor = CommandExecutor::new(mock_executor);
let result1 = executor.execute_task_by_name(&mut discovered_tasks, "grep", &[]);
assert!(result1.is_ok());
let result2 = executor.execute_task_by_name(&mut discovered_tasks, "grep-m", &[]);
assert!(result2.is_ok());
}
#[test]
fn test_execute_task_ambiguous_and_shadowed() {
let mut discovered_tasks = DiscoveredTasks::new();
let task1 = Task {
name: "test".to_string(),
file_path: PathBuf::from("/path/to/Makefile"),
definition_type: TaskDefinitionType::Makefile,
runner: TaskRunner::Make,
source_name: "test".to_string(),
description: None,
shadowed_by: Some(ShadowType::PathExecutable("/bin/test".to_string())),
disambiguated_name: Some("test-m".to_string()),
};
let task2 = Task {
name: "test".to_string(),
file_path: PathBuf::from("/path/to/package.json"),
definition_type: TaskDefinitionType::PackageJson,
runner: TaskRunner::NodeNpm,
source_name: "test".to_string(),
description: None,
shadowed_by: None,
disambiguated_name: Some("test-npm".to_string()),
};
discovered_tasks
.task_name_counts
.insert("test".to_string(), 2);
discovered_tasks.add_task(task1);
discovered_tasks.add_task(task2);
let mut mock_executor = MockTaskExecutor::new();
mock_executor.expect_execute().times(2).returning(|task| {
if task.runner == TaskRunner::Make {
assert_eq!(task.disambiguated_name, Some("test-m".to_string()));
} else if task.runner == TaskRunner::NodeNpm {
assert_eq!(task.disambiguated_name, Some("test-npm".to_string()));
} else {
panic!("Unexpected task runner");
}
Ok(())
});
let mut executor = CommandExecutor::new(mock_executor);
let result1 = executor.execute_task_by_name(&mut discovered_tasks, "test-m", &[]);
assert!(result1.is_ok());
let result2 = executor.execute_task_by_name(&mut discovered_tasks, "test-npm", &[]);
assert!(result2.is_ok());
let result3 = executor.execute_task_by_name(&mut discovered_tasks, "test", &[]);
assert!(result3.is_err());
let err_msg = result3.unwrap_err();
println!("Error message: {}", err_msg);
assert!(err_msg.contains("Ambiguous"));
}
#[test]
fn test_discover_taskfile_variants() {
let temp_dir = TempDir::new().unwrap();
let taskfile_yaml_content = r#"version: '3'
tasks:
from_yaml:
desc: This task is from taskfile.yaml
cmds:
- echo "From taskfile.yaml"
"#;
let taskfile_yaml_path = temp_dir.path().join("taskfile.yaml");
let mut file = File::create(&taskfile_yaml_path).unwrap();
write!(file, "{}", taskfile_yaml_content).unwrap();
let taskfile_yml_content = r#"version: '3'
tasks:
from_yml:
desc: This task is from Taskfile.yml
cmds:
- echo "From Taskfile.yml"
"#;
let taskfile_yml_path = temp_dir.path().join("Taskfile.yml");
let mut file = File::create(&taskfile_yml_path).unwrap();
write!(file, "{}", taskfile_yml_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let taskfile_def = discovered.definitions.taskfile.unwrap();
assert_eq!(taskfile_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 1);
let task = discovered.tasks.first().unwrap();
assert_eq!(task.name, "from_yml");
assert_eq!(
task.description,
Some("This task is from Taskfile.yml".to_string())
);
std::fs::remove_file(taskfile_yml_path).unwrap();
let discovered = discover_tasks(temp_dir.path());
let taskfile_def = discovered.definitions.taskfile.unwrap();
assert_eq!(taskfile_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 1);
let task = discovered.tasks.first().unwrap();
assert_eq!(task.name, "from_yaml");
assert_eq!(
task.description,
Some("This task is from taskfile.yaml".to_string())
);
}
#[test]
fn test_discover_docker_compose_tasks() {
let temp_dir = TempDir::new().unwrap();
let docker_compose_content = r#"
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
db:
image: postgres:13
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
app:
build: .
depends_on:
- db
"#;
let docker_compose_path = temp_dir.path().join("docker-compose.yml");
let mut file = File::create(&docker_compose_path).unwrap();
write!(file, "{}", docker_compose_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let docker_compose_def = discovered.definitions.docker_compose.unwrap();
assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed);
assert_eq!(docker_compose_def.path, docker_compose_path);
assert_eq!(discovered.tasks.len(), 5);
let service_names: Vec<&str> = discovered.tasks.iter().map(|t| t.name.as_str()).collect();
assert!(service_names.contains(&"up"));
assert!(service_names.contains(&"down"));
assert!(service_names.contains(&"web"));
assert!(service_names.contains(&"db"));
assert!(service_names.contains(&"app"));
for task in &discovered.tasks {
assert_eq!(task.definition_type, TaskDefinitionType::DockerCompose);
assert_eq!(task.runner, TaskRunner::DockerCompose);
assert_eq!(task.file_path, docker_compose_path);
assert!(task.description.is_some());
assert!(task.shadowed_by.is_none());
assert!(task.disambiguated_name.is_none());
}
let web_task = discovered.tasks.iter().find(|t| t.name == "web").unwrap();
assert!(web_task
.description
.as_ref()
.unwrap()
.contains("nginx:alpine"));
let app_task = discovered.tasks.iter().find(|t| t.name == "app").unwrap();
assert!(app_task.description.as_ref().unwrap().contains("build"));
}
#[test]
fn test_discover_docker_compose_empty() {
let temp_dir = TempDir::new().unwrap();
let docker_compose_content = r#"
version: '3.8'
services: {}
"#;
let docker_compose_path = temp_dir.path().join("docker-compose.yml");
let mut file = File::create(&docker_compose_path).unwrap();
write!(file, "{}", docker_compose_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let docker_compose_def = discovered.definitions.docker_compose.unwrap();
assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 2);
let service_names: Vec<&str> = discovered.tasks.iter().map(|t| t.name.as_str()).collect();
assert!(service_names.contains(&"up"));
assert!(service_names.contains(&"down"));
}
#[test]
fn test_discover_docker_compose_missing_file() {
let temp_dir = TempDir::new().unwrap();
let discovered = discover_tasks(temp_dir.path());
let docker_compose_def = discovered.definitions.docker_compose.unwrap();
assert_eq!(docker_compose_def.status, TaskFileStatus::NotFound);
assert_eq!(discovered.tasks.len(), 0);
}
#[test]
fn test_discover_docker_compose_multiple_formats() {
let temp_dir = TempDir::new().unwrap();
let compose_content = r#"
version: '3.8'
services:
api:
image: nginx:alpine
ports:
- "8080:80"
"#;
std::fs::write(temp_dir.path().join("compose.yml"), compose_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let docker_compose_def = discovered.definitions.docker_compose.unwrap();
assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed);
assert_eq!(docker_compose_def.path, temp_dir.path().join("compose.yml"));
assert_eq!(discovered.tasks.len(), 3);
let service_names: Vec<&str> = discovered.tasks.iter().map(|t| t.name.as_str()).collect();
assert!(service_names.contains(&"up"));
assert!(service_names.contains(&"down"));
assert!(service_names.contains(&"api"));
let api_task = discovered.tasks.iter().find(|t| t.name == "api").unwrap();
assert_eq!(api_task.definition_type, TaskDefinitionType::DockerCompose);
assert_eq!(api_task.runner, TaskRunner::DockerCompose);
let docker_compose_content = r#"
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
db:
image: postgres:13
"#;
std::fs::write(
temp_dir.path().join("docker-compose.yml"),
docker_compose_content,
)
.unwrap();
let discovered = discover_tasks(temp_dir.path());
let docker_compose_def = discovered.definitions.docker_compose.unwrap();
assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed);
assert_eq!(
docker_compose_def.path,
temp_dir.path().join("docker-compose.yml")
);
assert_eq!(discovered.tasks.len(), 4);
let service_names: Vec<&str> = discovered.tasks.iter().map(|t| t.name.as_str()).collect();
assert!(service_names.contains(&"up"));
assert!(service_names.contains(&"down"));
assert!(service_names.contains(&"web"));
assert!(service_names.contains(&"db"));
}
#[test]
fn test_discover_travis_ci_tasks() {
let temp_dir = TempDir::new().unwrap();
let travis_content = r#"
language: node_js
node_js:
- "18"
- "20"
jobs:
test:
name: "Test"
stage: test
build:
name: "Build"
stage: build
"#;
let travis_path = temp_dir.path().join(".travis.yml");
let mut file = File::create(&travis_path).unwrap();
write!(file, "{}", travis_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let travis_def = discovered.definitions.travis_ci.unwrap();
assert_eq!(travis_def.status, TaskFileStatus::Parsed);
assert_eq!(travis_def.path, travis_path);
assert_eq!(discovered.tasks.len(), 2);
let test_task = discovered.tasks.iter().find(|t| t.name == "test").unwrap();
assert_eq!(test_task.definition_type, TaskDefinitionType::TravisCi);
assert_eq!(test_task.runner, TaskRunner::TravisCi);
assert_eq!(
test_task.description,
Some("Travis CI job: Test".to_string())
);
let build_task = discovered.tasks.iter().find(|t| t.name == "build").unwrap();
assert_eq!(build_task.definition_type, TaskDefinitionType::TravisCi);
assert_eq!(build_task.runner, TaskRunner::TravisCi);
assert_eq!(
build_task.description,
Some("Travis CI job: Build".to_string())
);
}
#[test]
fn test_discover_travis_ci_matrix_config() {
let temp_dir = TempDir::new().unwrap();
let travis_content = r#"
language: python
matrix:
include:
- name: "Python 3.8"
python: "3.8"
- name: "Python 3.9"
python: "3.9"
- name: "Python 3.10"
python: "3.10"
"#;
let travis_path = temp_dir.path().join(".travis.yml");
let mut file = File::create(&travis_path).unwrap();
write!(file, "{}", travis_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let travis_def = discovered.definitions.travis_ci.unwrap();
assert_eq!(travis_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 3);
for task in &discovered.tasks {
assert_eq!(task.definition_type, TaskDefinitionType::TravisCi);
assert_eq!(task.runner, TaskRunner::TravisCi);
assert!(task
.description
.as_ref()
.unwrap()
.contains("Travis CI job:"));
}
let python_38_task = discovered
.tasks
.iter()
.find(|t| t.name == "Python 3.8")
.unwrap();
assert_eq!(
python_38_task.description,
Some("Travis CI job: Python 3.8".to_string())
);
let python_39_task = discovered
.tasks
.iter()
.find(|t| t.name == "Python 3.9")
.unwrap();
assert_eq!(
python_39_task.description,
Some("Travis CI job: Python 3.9".to_string())
);
let python_310_task = discovered
.tasks
.iter()
.find(|t| t.name == "Python 3.10")
.unwrap();
assert_eq!(
python_310_task.description,
Some("Travis CI job: Python 3.10".to_string())
);
}
#[test]
fn test_discover_travis_ci_basic_config() {
let temp_dir = TempDir::new().unwrap();
let travis_content = r#"
language: ruby
rvm:
- 2.7
- 3.0
- 3.1
script:
- bundle install
- bundle exec rspec
"#;
let travis_path = temp_dir.path().join(".travis.yml");
let mut file = File::create(&travis_path).unwrap();
write!(file, "{}", travis_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let travis_def = discovered.definitions.travis_ci.unwrap();
assert_eq!(travis_def.status, TaskFileStatus::Parsed);
assert_eq!(discovered.tasks.len(), 1);
let task = &discovered.tasks[0];
assert_eq!(task.name, "travis");
assert_eq!(task.definition_type, TaskDefinitionType::TravisCi);
assert_eq!(task.runner, TaskRunner::TravisCi);
assert_eq!(
task.description,
Some("Travis CI configuration".to_string())
);
}
#[test]
fn test_discover_travis_ci_missing_file() {
let temp_dir = TempDir::new().unwrap();
let discovered = discover_tasks(temp_dir.path());
let travis_def = discovered.definitions.travis_ci.unwrap();
assert_eq!(travis_def.status, TaskFileStatus::NotFound);
assert_eq!(discovered.tasks.len(), 0);
}
#[test]
fn test_discover_cmake_tasks() {
let temp_dir = TempDir::new().unwrap();
let cmake_content = r#"
cmake_minimum_required(VERSION 3.10)
project(TestProject)
add_custom_target(build-all COMMENT "Build all components")
add_custom_target(test-all COMMENT "Run all tests")
add_custom_target(clean-all COMMENT "Clean all build artifacts")
"#;
let cmake_path = temp_dir.path().join("CMakeLists.txt");
let mut file = File::create(&cmake_path).unwrap();
write!(file, "{}", cmake_content).unwrap();
let discovered = discover_tasks(temp_dir.path());
let cmake_def = discovered.definitions.cmake.unwrap();
assert_eq!(cmake_def.status, TaskFileStatus::Parsed);
assert_eq!(cmake_def.path, cmake_path);
let task_names: Vec<&str> = discovered.tasks.iter().map(|t| t.name.as_str()).collect();
assert!(task_names.contains(&"build-all"));
assert!(task_names.contains(&"test-all"));
assert!(task_names.contains(&"clean-all"));
for task in &discovered.tasks {
if task.name == "build-all" || task.name == "test-all" || task.name == "clean-all" {
assert_eq!(task.runner, TaskRunner::CMake);
assert_eq!(task.definition_type, TaskDefinitionType::CMake);
}
}
}
}