diff --git a/cargo_crap_baseline.json b/cargo_crap_baseline.json index cd30ea8..2c0932c 100644 --- a/cargo_crap_baseline.json +++ b/cargo_crap_baseline.json @@ -2,22 +2,6 @@ "$schema": "https://raw.githubusercontent.com/minikin/cargo-crap/main/schemas/report-v1.json", "version": "0.2.0", "entries": [ - { - "file": "./src/commands/list.rs", - "function": "execute", - "line": 20, - "cyclomatic": 72.0, - "coverage": 0.0, - "crap": 5256.0 - }, - { - "file": "./src/commands/mcp.rs", - "function": "execute", - "line": 243, - "cyclomatic": 19.0, - "coverage": 0.0, - "crap": 380.0 - }, { "file": "./src/mcp/job_manager.rs", "function": "JobManager::stop_job_graceful", @@ -42,14 +26,6 @@ "coverage": 0.0, "crap": 182.0 }, - { - "file": "./src/main.rs", - "function": "main", - "line": 163, - "cyclomatic": 11.0, - "coverage": 0.0, - "crap": 132.0 - }, { "file": "./src/mcp/server.rs", "function": "DelaMcpServer::task_start", @@ -58,6 +34,14 @@ "coverage": 69.86899563318777, "crap": 93.57987320997631 }, + { + "file": "./src/commands/list.rs", + "function": "execute", + "line": 22, + "cyclomatic": 43.0, + "coverage": 69.93464052287581, + "crap": 93.25000551433072 + }, { "file": "./src/prompt.rs", "function": "prompt_for_task", @@ -66,6 +50,22 @@ "coverage": 27.27272727272727, "crap": 57.54545454545455 }, + { + "file": "./src/commands/mcp.rs", + "function": "execute", + "line": 243, + "cyclomatic": 19.0, + "coverage": 55.55555555555556, + "crap": 50.692729766803836 + }, + { + "file": "./src/main.rs", + "function": "run_command", + "line": 162, + "cyclomatic": 9.0, + "coverage": 22.22222222222222, + "crap": 47.111111111111114 + }, { "file": "./src/mcp/job_manager.rs", "function": "JobManager::garbage_collect", @@ -109,7 +109,7 @@ { "file": "./src/types.rs", "function": "TaskRunner::get_command", - "line": 199, + "line": 205, "cyclomatic": 21.0, "coverage": 100.0, "crap": 21.0 @@ -165,7 +165,7 @@ { "file": "./src/types.rs", "function": "TaskRunner::short_name", - "line": 242, + "line": 248, "cyclomatic": 19.0, "coverage": 95.23809523809523, "crap": 19.038980671633734 @@ -221,7 +221,7 @@ { "file": "./src/commands/list.rs", "function": "format_task_entry", - "line": 356, + "line": 235, "cyclomatic": 15.0, "coverage": 83.33333333333334, "crap": 16.041666666666664 @@ -234,21 +234,13 @@ "coverage": 92.3076923076923, "crap": 15.10241238051889 }, - { - "file": "./src/task_discovery/support.rs", - "function": "set_definition", - "line": 12, - "cyclomatic": 14.0, - "coverage": 87.5, - "crap": 14.3828125 - }, { "file": "./src/allowlist.rs", "function": "evaluate_task_against_allowlist", "line": 75, "cyclomatic": 14.0, - "coverage": 95.65217391304348, - "crap": 14.016109147694584 + "coverage": 100.0, + "crap": 14.0 }, { "file": "./src/mcp/server.rs", @@ -306,6 +298,14 @@ "coverage": 95.0, "crap": 12.018 }, + { + "file": "./src/main.rs", + "function": "main", + "line": 198, + "cyclomatic": 3.0, + "coverage": 0.0, + "crap": 12.0 + }, { "file": "./src/parsers/parse_makefile.rs", "function": "parse", @@ -922,14 +922,6 @@ "coverage": 86.20689655172413, "crap": 3.0236172044774285 }, - { - "file": "./src/commands/list.rs", - "function": "format_task_entry_with_source", - "line": 445, - "cyclomatic": 3.0, - "coverage": 87.5, - "crap": 3.017578125 - }, { "file": "./src/task_discovery/turbo.rs", "function": "resolve_turbo_extends_entry", @@ -1074,10 +1066,18 @@ "coverage": 100.0, "crap": 3.0 }, + { + "file": "./src/commands/list.rs", + "function": "format_task_entry_with_source", + "line": 324, + "cyclomatic": 3.0, + "coverage": 100.0, + "crap": 3.0 + }, { "file": "./src/commands/list.rs", "function": "task_source_label", - "line": 458, + "line": 337, "cyclomatic": 3.0, "coverage": 100.0, "crap": 3.0 @@ -1125,7 +1125,7 @@ { "file": "./src/commands/list.rs", "function": "format_definition_path_for_display", - "line": 472, + "line": 351, "cyclomatic": 2.0, "coverage": 80.0, "crap": 2.032 @@ -1141,7 +1141,7 @@ { "file": "./src/commands/list.rs", "function": "format_runner_path_for_display", - "line": 480, + "line": 359, "cyclomatic": 2.0, "coverage": 85.71428571428571, "crap": 2.011661807580175 @@ -1205,7 +1205,7 @@ { "file": "./src/types.rs", "function": "deserialize_path", - "line": 315, + "line": 309, "cyclomatic": 2.0, "coverage": 100.0, "crap": 2.0 @@ -1594,18 +1594,10 @@ "coverage": 100.0, "crap": 2.0 }, - { - "file": "./src/prompt.rs", - "function": "ui", - "line": 169, - "cyclomatic": 1.0, - "coverage": 0.0, - "crap": 2.0 - }, { "file": "./src/task_discovery.rs", "function": "discover_tasks", - "line": 67, + "line": 51, "cyclomatic": 2.0, "coverage": 100.0, "crap": 2.0 @@ -1618,14 +1610,6 @@ "coverage": 0.0, "crap": 2.0 }, - { - "file": "./src/commands/mcp.rs", - "function": "generate_config", - "line": 237, - "cyclomatic": 1.0, - "coverage": 0.0, - "crap": 2.0 - }, { "file": "./src/environment.rs", "function": "TestEnvironment::check_executable", @@ -1690,10 +1674,42 @@ "coverage": 100.0, "crap": 1.0 }, + { + "file": "./src/types.rs", + "function": "DiscoveredTaskDefinitions::insert", + "line": 139, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, + { + "file": "./src/types.rs", + "function": "DiscoveredTaskDefinitions::get_first", + "line": 148, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, + { + "file": "./src/types.rs", + "function": "DiscoveredTaskDefinitions::get_all", + "line": 154, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, + { + "file": "./src/types.rs", + "function": "DiscoveredTaskDefinitions::iter", + "line": 159, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, { "file": "./src/types.rs", "function": "Task::definition_path", - "line": 187, + "line": 193, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -1701,7 +1717,7 @@ { "file": "./src/types.rs", "function": "Task::allowlist_path", - "line": 192, + "line": 198, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -1709,7 +1725,7 @@ { "file": "./src/types.rs", "function": "serialize_path", - "line": 308, + "line": 302, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -1794,10 +1810,18 @@ "coverage": 100.0, "crap": 1.0 }, + { + "file": "./src/task_discovery/support.rs", + "function": "set_definition", + "line": 12, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, { "file": "./src/task_discovery/support.rs", "function": "handle_discovery_error", - "line": 36, + "line": 16, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -1805,7 +1829,7 @@ { "file": "./src/task_discovery/support.rs", "function": "handle_discovery_success", - "line": 57, + "line": 37, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -2306,10 +2330,18 @@ "coverage": 100.0, "crap": 1.0 }, + { + "file": "./src/prompt.rs", + "function": "ui", + "line": 169, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, { "file": "./src/task_discovery.rs", "function": "DiscoveredTasks::new", - "line": 52, + "line": 36, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 @@ -2317,7 +2349,15 @@ { "file": "./src/task_discovery.rs", "function": "DiscoveredTasks::add_task", - "line": 57, + "line": 41, + "cyclomatic": 1.0, + "coverage": 100.0, + "crap": 1.0 + }, + { + "file": "./src/commands/mcp.rs", + "function": "generate_config", + "line": 237, "cyclomatic": 1.0, "coverage": 100.0, "crap": 1.0 diff --git a/dev_docs/project_plan.md b/dev_docs/project_plan.md index 3a63427..041316a 100644 --- a/dev_docs/project_plan.md +++ b/dev_docs/project_plan.md @@ -345,10 +345,10 @@ Advanced MCP features for better editor integration and real-time feedback. ## Technical Debt & Code Cleanup (Phase 11) -- [ ] **[DTKT-202]** Refactor `DiscoveredTaskDefinitions` to use a grouped map collection (`BTreeMap>`) instead of hardcoded fields. This preserves predictable output ordering while robustly capturing multiple definitions per parser (e.g., included Makefiles or workspace-local `turbo.json` configs). -- [ ] **[DTKT-203]** Migrate all dependent discovery and output layers to iterate dynamically over the new collection, allowing new parsers (e.g., TravisCi, CMake, Justfile) to correctly store their statuses. +- [x] **[DTKT-202]** Refactor `DiscoveredTaskDefinitions` to use a grouped map collection (`BTreeMap>`) instead of hardcoded fields. This preserves predictable output ordering while robustly capturing multiple definitions per parser (e.g., included Makefiles or workspace-local `turbo.json` configs). +- [x] **[DTKT-203]** Migrate all dependent discovery and output layers to iterate dynamically over the new collection, allowing new parsers (e.g., TravisCi, CMake, Justfile) to correctly store their statuses. -- [ ] **[DTKT-204]** Consolidate the 100+ lines of duplicated `TaskFileStatus` match formatting blocks in `src/commands/list.rs` into a single loop mapping over the refactored `DiscoveredTaskDefinitions`. +- [x] **[DTKT-204]** Consolidate the 100+ lines of duplicated `TaskFileStatus` match formatting blocks in `src/commands/list.rs` into a single loop mapping over the refactored `DiscoveredTaskDefinitions`. - [ ] **[DTKT-205]** Deprecate the hacky `test_println!` macro and abstract CLI formatting: Inject a `&mut dyn std::io::Write` interface (or an abstract output buffer) across commands. - [ ] **[DTKT-206]** Update tests in `list.rs` to pass a mock buffer and write explicit assertions against the output strings instead of discarding stdout. diff --git a/src/commands/list.rs b/src/commands/list.rs index a63b602..d22ec91 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -10,7 +10,9 @@ use std::path::Path; #[cfg(test)] macro_rules! test_println { - ($($arg:tt)*) => {}; + ($($arg:tt)*) => { + let _ = format_args!($($arg)*); + }; } #[cfg(not(test))] macro_rules! test_println { @@ -31,156 +33,33 @@ pub fn execute(verbose: bool, color: &str) -> anyhow::Result<()> { // Only show task definition files status in verbose mode if verbose { test_println!("Task definition files:"); - if let Some(makefile) = &discovered.definitions.makefile { - match &makefile.status { - TaskFileStatus::Parsed => { - test_println!(" {} Makefile: Found and parsed", "✓".green()); - } - TaskFileStatus::NotImplemented => { - test_println!( - " {} Makefile: Found but parsing not yet implemented", - "!".yellow() - ); - } - TaskFileStatus::ParseError(_e) => { - test_println!(" {} Makefile: Error parsing: {}", "✗".red(), _e); - } - TaskFileStatus::NotReadable(_e) => { - test_println!(" {} Makefile: Not readable: {}", "✗".red(), _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} Makefile: Not found", "-".dimmed()); - } - } - } - if let Some(package_json) = &discovered.definitions.package_json { - match &package_json.status { - TaskFileStatus::Parsed => { - test_println!(" {} package.json: Found and parsed", "✓".green()); - } - TaskFileStatus::NotImplemented => { - test_println!( - " {} package.json: Found but parsing not yet implemented", - "!".yellow() - ); - } - TaskFileStatus::ParseError(_e) => { - test_println!(" {} package.json: Error parsing: {}", "✗".red(), _e); - } - TaskFileStatus::NotReadable(_e) => { - test_println!(" {} package.json: Not readable: {}", "✗".red(), _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} package.json: Not found", "-".dimmed()); - } - } - } - if let Some(pyproject_toml) = &discovered.definitions.pyproject_toml { - match &pyproject_toml.status { - TaskFileStatus::Parsed => { - test_println!(" {} pyproject.toml: Found and parsed", "✓".green()); - } - TaskFileStatus::NotImplemented => { - test_println!( - " {} pyproject.toml: Found but parsing not yet implemented", - "!".yellow() - ); - } - TaskFileStatus::ParseError(_e) => { - test_println!(" {} pyproject.toml: Error parsing: {}", "✗".red(), _e); - } - TaskFileStatus::NotReadable(_e) => { - test_println!(" {} pyproject.toml: Not readable: {}", "✗".red(), _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} pyproject.toml: Not found", "-".dimmed()); - } - } - } - if let Some(turbo_json) = &discovered.definitions.turbo_json { - match &turbo_json.status { - TaskFileStatus::Parsed => { - test_println!(" {} turbo.json: Found and parsed", "✓".green()); - } - TaskFileStatus::NotImplemented => { - test_println!( - " {} turbo.json: Found but parsing not yet implemented", - "!".yellow() - ); - } - TaskFileStatus::ParseError(_e) => { - test_println!(" {} turbo.json: Error parsing: {}", "✗".red(), _e); - } - TaskFileStatus::NotReadable(_e) => { - test_println!(" {} turbo.json: Not readable: {}", "✗".red(), _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} turbo.json: Not found", "-".dimmed()); - } - } - } - if let Some(maven_pom) = &discovered.definitions.maven_pom { - match &maven_pom.status { - TaskFileStatus::Parsed => { - test_println!(" {} pom.xml: Found and parsed", "✓".green()); - } - TaskFileStatus::NotImplemented => { - test_println!( - " {} pom.xml: Found but parsing not yet implemented", - "!".yellow() - ); - } - TaskFileStatus::ParseError(_e) => { - test_println!(" {} pom.xml: Error parsing: {}", "✗".red(), _e); - } - TaskFileStatus::NotReadable(_e) => { - test_println!(" {} pom.xml: Not readable: {}", "✗".red(), _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} pom.xml: Not found", "-".dimmed()); - } - } - } - if let Some(gradle) = &discovered.definitions.gradle { - match &gradle.status { - TaskFileStatus::Parsed => { - let _file_name = gradle - .path - .file_name() - .unwrap_or_default() - .to_string_lossy(); - test_println!(" {} {}: Found and parsed", "✓".green(), _file_name); - } - TaskFileStatus::NotImplemented => { - let _file_name = gradle - .path - .file_name() - .unwrap_or_default() - .to_string_lossy(); - test_println!( - " {} {}: Found but parsing not yet implemented", - "!".yellow(), - _file_name - ); - } - TaskFileStatus::ParseError(_e) => { - let _file_name = gradle - .path - .file_name() - .unwrap_or_default() - .to_string_lossy(); - test_println!(" {} {}: Error parsing: {}", "✗".red(), _file_name, _e); - } - TaskFileStatus::NotReadable(_e) => { - let _file_name = gradle - .path - .file_name() - .unwrap_or_default() - .to_string_lossy(); - test_println!(" {} {}: Not readable: {}", "✗".red(), _file_name, _e); - } - TaskFileStatus::NotFound => { - test_println!(" {} Gradle build file: Not found", "-".dimmed()); + for (_def_type, files) in discovered.definitions.iter() { + for file in files { + let file_name = file + .path + .strip_prefix(¤t_dir) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| file.path.to_string_lossy().to_string()); + match &file.status { + TaskFileStatus::Parsed => { + test_println!(" {} {}: Found and parsed", "✓".green(), file_name); + } + TaskFileStatus::NotImplemented => { + test_println!( + " {} {}: Found but parsing not yet implemented", + "!".yellow(), + file_name + ); + } + TaskFileStatus::ParseError(e) => { + test_println!(" {} {}: Error parsing: {}", "✗".red(), file_name, e); + } + TaskFileStatus::NotReadable(e) => { + test_println!(" {} {}: Not readable: {}", "✗".red(), file_name, e); + } + TaskFileStatus::NotFound => { + test_println!(" {} {}: Not found", "-".dimmed(), file_name); + } } } } @@ -1068,4 +947,38 @@ mod tests { Some("mk/common.mk".to_string()) ); } + + struct CwdGuard { + old_dir: Option, + } + + impl Drop for CwdGuard { + fn drop(&mut self) { + if let Some(ref dir) = self.old_dir { + let _ = std::env::set_current_dir(dir); + } + reset_to_real_environment(); + } + } + + #[test] + #[serial] + fn test_execute_command_success() { + let (temp_dir, _home_dir) = setup_test_env(); + let original_dir = std::env::current_dir().ok(); + let _guard = CwdGuard { + old_dir: original_dir, + }; + + // Change current directory to temp_dir + std::env::set_current_dir(temp_dir.path()).unwrap(); + + // Create a dummy Makefile + let makefile_path = temp_dir.path().join("Makefile"); + std::fs::write(&makefile_path, "build:\n\techo 'building'\n").unwrap(); + + // Run execute + let result = execute(true, "never"); + assert!(result.is_ok()); + } } diff --git a/src/commands/mcp.rs b/src/commands/mcp.rs index ff0c2f0..63f85ae 100644 --- a/src/commands/mcp.rs +++ b/src/commands/mcp.rs @@ -487,4 +487,53 @@ mod tests { home.join(".claude-code/settings.json") ); } + + struct TestEnvGuard { + old_dir: Option, + old_home: Option, + } + + impl Drop for TestEnvGuard { + fn drop(&mut self) { + if let Some(ref dir) = self.old_dir { + let _ = std::env::set_current_dir(dir); + } + if let Some(ref home) = self.old_home { + unsafe { + std::env::set_var("HOME", home); + } + } else { + unsafe { + std::env::remove_var("HOME"); + } + } + } + } + + #[tokio::test] + #[serial_test::serial] + async fn test_execute_init_cursor() { + let temp_dir = TempDir::new().unwrap(); + // Save the old HOME env var and original CWD inside RAII guard + let _guard = TestEnvGuard { + old_dir: std::env::current_dir().ok(), + old_home: std::env::var("HOME").ok(), + }; + + // Change current directory to temp_dir + std::env::set_current_dir(temp_dir.path()).unwrap(); + + unsafe { + std::env::set_var("HOME", temp_dir.path()); + } + + let result = execute(".".to_string(), true, false, false, false, false).await; + if let Err(ref e) = result { + panic!("execute failed with error: {:?}", e); + } + assert!(result.is_ok()); + + let expected_path = temp_dir.path().join(".cursor/mcp.json"); + assert!(expected_path.exists()); + } } diff --git a/src/main.rs b/src/main.rs index 3308ff4..8a809b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,11 +159,8 @@ enum Commands { }, } -#[tokio::main] -async fn main() { - let cli = Cli::parse(); - - let result = match cli.command { +async fn run_command(command: Commands) -> anyhow::Result<()> { + match command { Commands::Mcp { cwd, init_cursor, @@ -194,7 +191,14 @@ async fn main() { } } Commands::AllowCommand { task, allow } => commands::allow_command::execute(&task, allow), - }; + } +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + let result = run_command(cli.command).await; if let Err(err) = result { if err @@ -211,6 +215,7 @@ async fn main() { #[cfg(test)] mod tests { + use super::{Commands, run_command}; use std::io::Write; use tempfile::NamedTempFile; @@ -254,4 +259,11 @@ mod tests { "Regular error should have 'Error:' prefix" ); } + + #[tokio::test] + async fn test_run_command_get_command_empty() { + let result = run_command(Commands::GetCommand { args: vec![] }).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "No task name provided"); + } } diff --git a/src/prompt.rs b/src/prompt.rs index ddc5bcd..a9aa09f 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -318,4 +318,51 @@ mod tests { assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "Invalid selection index"); } + + #[test] + fn test_ui_rendering() { + use crate::types::{Task, TaskDefinitionType, TaskRunner}; + use ratatui::Terminal; + use ratatui::backend::TestBackend; + use std::path::PathBuf; + + let backend = TestBackend::new(80, 20); + let mut terminal = Terminal::new(backend).unwrap(); + + let task = Task { + name: "test-task".to_string(), + file_path: PathBuf::from("Makefile"), + definition_path: None, + definition_type: TaskDefinitionType::Makefile, + runner: TaskRunner::Make, + source_name: "test-task".to_string(), + description: Some("Run unit tests".to_string()), + shadowed_by: None, + disambiguated_name: None, + }; + + let options = vec![ + ( + "Allow once (this time only)", + AllowDecision::Allow(AllowScope::Once), + ), + ("Deny (don't run this task)", AllowDecision::Deny), + ]; + + terminal.draw(|f| ui(f, &task, &options, 0)).unwrap(); + + let buffer = terminal.backend().buffer(); + let contents = (0..20) + .map(|y| { + (0..80) + .map(|x| buffer[(x as u16, y as u16)].symbol().to_string()) + .collect::() + }) + .collect::>() + .join("\n"); + + assert!(contents.contains("test-task")); + assert!(contents.contains("Allow once")); + assert!(contents.contains("Deny")); + } } diff --git a/src/task_discovery.rs b/src/task_discovery.rs index df3873b..1e06aa6 100644 --- a/src/task_discovery.rs +++ b/src/task_discovery.rs @@ -15,7 +15,7 @@ mod taskfile; mod travis_ci; mod turbo; -use crate::types::{Task, TaskDefinitionFile}; +use crate::types::{DiscoveredTaskDefinitions, Task, TaskDefinitionFile}; use std::collections::HashMap; use std::path::Path; @@ -23,22 +23,6 @@ pub use disambiguation::{ format_ambiguous_task_error, get_matching_tasks, is_task_ambiguous, process_task_disambiguation, }; -#[derive(Debug, Clone, Default)] -pub struct DiscoveredTaskDefinitions { - pub makefile: Option, - pub package_json: Option, - pub pyproject_toml: Option, - pub taskfile: Option, - pub turbo_json: Option, - pub maven_pom: Option, - pub gradle: Option, - pub github_actions: Option, - pub docker_compose: Option, - pub travis_ci: Option, - pub cmake: Option, - pub justfile: Option, -} - #[derive(Debug, Clone, Default)] pub struct DiscoveredTasks { pub definitions: DiscoveredTaskDefinitions, @@ -210,24 +194,40 @@ mod tests { // Check Makefile status assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::NotFound )); // Check package.json status assert!(matches!( - discovered.definitions.package_json.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::PackageJson) + .unwrap() + .status, TaskFileStatus::NotFound )); // Check pyproject.toml status assert!(matches!( - discovered.definitions.pyproject_toml.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::PyprojectToml) + .unwrap() + .status, TaskFileStatus::NotFound )); assert!(matches!( - discovered.definitions.turbo_json.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::TurboJson) + .unwrap() + .status, TaskFileStatus::NotFound )); } @@ -252,7 +252,10 @@ test: assert!(discovered.errors.is_empty()); // Check Makefile status - let makefile_def = discovered.definitions.makefile.as_ref().unwrap(); + let makefile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap(); assert!(matches!(makefile_def.status, TaskFileStatus::Parsed)); assert_eq!(makefile_def.path, temp_dir.path().join("Makefile")); @@ -283,11 +286,19 @@ test: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.as_ref().unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!( - discovered.definitions.makefile.as_ref().unwrap().path, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .path, temp_dir.path().join("makefile") ); assert_eq!(discovered.tasks.len(), 1); @@ -310,11 +321,19 @@ test: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.as_ref().unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!( - discovered.definitions.makefile.as_ref().unwrap().path, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .path, temp_dir.path().join("GNUmakefile") ); assert_eq!(discovered.tasks.len(), 1); @@ -343,7 +362,11 @@ test: let discovered = discover_tasks(temp_dir.path()); assert_eq!( - discovered.definitions.makefile.as_ref().unwrap().path, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .path, temp_dir.path().join("GNUmakefile") ); assert!( @@ -392,7 +415,11 @@ test: assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!(discovered.tasks.len(), 3); @@ -496,7 +523,11 @@ build: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); @@ -529,7 +560,11 @@ test: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); @@ -552,7 +587,11 @@ build: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); @@ -581,7 +620,11 @@ build: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!(discovered.tasks.len(), 1); @@ -616,7 +659,11 @@ include mk/valid.mk"#, let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!(discovered.tasks.len(), 1); @@ -667,7 +714,11 @@ include mk/valid.mk"#, ); assert_eq!( - discovered.definitions.turbo_json.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::TurboJson) + .unwrap() + .status, TaskFileStatus::Parsed ); } @@ -864,7 +915,11 @@ include mk/valid.mk"#, // The status should be ParseError, as the makefile contains invalid content: assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::ParseError(_) )); } @@ -882,7 +937,11 @@ include mk/valid.mk"#, // Check pyproject.toml status - should be ParseError now that we've implemented it assert!(matches!( - discovered.definitions.pyproject_toml.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::PyprojectToml) + .unwrap() + .status, TaskFileStatus::ParseError(_) )); } @@ -916,7 +975,10 @@ include mk/valid.mk"#, let discovered = discover_tasks(temp_dir.path()); // Check package.json status - let package_json_def = discovered.definitions.package_json.unwrap(); + let package_json_def = discovered + .definitions + .get_first(&TaskDefinitionType::PackageJson) + .unwrap(); assert_eq!(package_json_def.status, TaskFileStatus::Parsed); // Verify tasks were discovered @@ -953,7 +1015,10 @@ include mk/valid.mk"#, let discovered = discover_tasks(temp_dir.path()); // Check package.json status shows parse error - let package_json_def = discovered.definitions.package_json.unwrap(); + let package_json_def = discovered + .definitions + .get_first(&TaskDefinitionType::PackageJson) + .unwrap(); assert!(matches!( package_json_def.status, TaskFileStatus::ParseError(_) @@ -989,7 +1054,10 @@ serve = "uvicorn main:app --reload" let discovered = discover_tasks(temp_dir.path()); // Check pyproject.toml status - let pyproject_def = discovered.definitions.pyproject_toml.unwrap(); + let pyproject_def = discovered + .definitions + .get_first(&TaskDefinitionType::PyprojectToml) + .unwrap(); assert_eq!(pyproject_def.status, TaskFileStatus::Parsed); // Verify tasks were discovered @@ -1036,7 +1104,10 @@ lint = "flake8" let discovered = discover_tasks(temp_dir.path()); // Check pyproject.toml status - let pyproject_def = discovered.definitions.pyproject_toml.unwrap(); + let pyproject_def = discovered + .definitions + .get_first(&TaskDefinitionType::PyprojectToml) + .unwrap(); assert_eq!(pyproject_def.status, TaskFileStatus::Parsed); // Verify tasks were discovered @@ -1123,15 +1194,27 @@ serve = "python -m http.server" // Verify all task files were parsed assert!(matches!( - discovered.definitions.makefile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Makefile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert!(matches!( - discovered.definitions.package_json.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::PackageJson) + .unwrap() + .status, TaskFileStatus::Parsed )); assert!(matches!( - discovered.definitions.pyproject_toml.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::PyprojectToml) + .unwrap() + .status, TaskFileStatus::Parsed )); @@ -1377,7 +1460,10 @@ tasks: let discovered = discover_tasks(temp_dir.path()); // Check Taskfile.yml status - let taskfile_def = discovered.definitions.taskfile.unwrap(); + let taskfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap(); assert_eq!(taskfile_def.status, TaskFileStatus::Parsed); // Verify tasks were discovered @@ -1452,7 +1538,11 @@ tasks: assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); assert!(matches!( - discovered.definitions.taskfile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap() + .status, TaskFileStatus::Parsed )); @@ -1517,7 +1607,11 @@ tasks: assert!(discovered.errors.is_empty(), "{:?}", discovered.errors); assert!(matches!( - discovered.definitions.taskfile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap() + .status, TaskFileStatus::Parsed )); assert_eq!(discovered.tasks.len(), 1); @@ -1572,7 +1666,11 @@ tasks: let discovered = discover_tasks(temp_dir.path()); assert!(matches!( - discovered.definitions.taskfile.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap() + .status, TaskFileStatus::ParseError(_) )); assert!( @@ -1660,9 +1758,18 @@ tasks: let discovered = discover_tasks(dir_path); // Check that the definition was found - assert!(discovered.definitions.maven_pom.is_some()); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::MavenPom) + .is_some() + ); assert_eq!( - discovered.definitions.maven_pom.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::MavenPom) + .unwrap() + .status, TaskFileStatus::Parsed ); @@ -1835,7 +1942,11 @@ jobs: // Check GitHub Actions status assert!(matches!( - discovered.definitions.github_actions.unwrap().status, + discovered + .definitions + .get_first(&TaskDefinitionType::GitHubActions) + .unwrap() + .status, TaskFileStatus::Parsed )); @@ -2376,7 +2487,10 @@ tasks: let discovered = discover_tasks(temp_dir.path()); // Check that the taskfile status is Parsed - let taskfile_def = discovered.definitions.taskfile.unwrap(); + let taskfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap(); assert_eq!(taskfile_def.status, TaskFileStatus::Parsed); // Verify the task from Taskfile.yml exists (check by content rather than filename) @@ -2395,7 +2509,10 @@ tasks: let discovered = discover_tasks(temp_dir.path()); // Check that the taskfile status is Parsed - let taskfile_def = discovered.definitions.taskfile.unwrap(); + let taskfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Taskfile) + .unwrap(); assert_eq!(taskfile_def.status, TaskFileStatus::Parsed); // Check the task from taskfile.yaml exists (verify by content) @@ -2439,7 +2556,10 @@ services: let discovered = discover_tasks(temp_dir.path()); // Check that the docker-compose status is Parsed - let docker_compose_def = discovered.definitions.docker_compose.unwrap(); + let docker_compose_def = discovered + .definitions + .get_first(&TaskDefinitionType::DockerCompose) + .unwrap(); assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed); assert_eq!(docker_compose_def.path, docker_compose_path); @@ -2494,7 +2614,10 @@ services: {} let discovered = discover_tasks(temp_dir.path()); // Check that the docker-compose status is Parsed - let docker_compose_def = discovered.definitions.docker_compose.unwrap(); + let docker_compose_def = discovered + .definitions + .get_first(&TaskDefinitionType::DockerCompose) + .unwrap(); assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed); // Check that only the "up" and "down" tasks are found @@ -2512,7 +2635,10 @@ services: {} let discovered = discover_tasks(temp_dir.path()); // Check that the docker-compose status is NotFound - let docker_compose_def = discovered.definitions.docker_compose.unwrap(); + let docker_compose_def = discovered + .definitions + .get_first(&TaskDefinitionType::DockerCompose) + .unwrap(); assert_eq!(docker_compose_def.status, TaskFileStatus::NotFound); // Check that no tasks are found @@ -2538,7 +2664,10 @@ services: let discovered = discover_tasks(temp_dir.path()); // Check that the docker-compose status is Parsed - let docker_compose_def = discovered.definitions.docker_compose.unwrap(); + let docker_compose_def = discovered + .definitions + .get_first(&TaskDefinitionType::DockerCompose) + .unwrap(); assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed); assert_eq!(docker_compose_def.path, temp_dir.path().join("compose.yml")); @@ -2574,7 +2703,10 @@ services: let discovered = discover_tasks(temp_dir.path()); // Check that the higher priority file is used - let docker_compose_def = discovered.definitions.docker_compose.unwrap(); + let docker_compose_def = discovered + .definitions + .get_first(&TaskDefinitionType::DockerCompose) + .unwrap(); assert_eq!(docker_compose_def.status, TaskFileStatus::Parsed); assert_eq!( docker_compose_def.path, @@ -2617,7 +2749,10 @@ jobs: let discovered = discover_tasks(temp_dir.path()); // Check that the travis-ci status is Parsed - let travis_def = discovered.definitions.travis_ci.unwrap(); + let travis_def = discovered + .definitions + .get_first(&TaskDefinitionType::TravisCi) + .unwrap(); assert_eq!(travis_def.status, TaskFileStatus::Parsed); assert_eq!(travis_def.path, travis_path); @@ -2666,7 +2801,10 @@ matrix: let discovered = discover_tasks(temp_dir.path()); // Check that the travis-ci status is Parsed - let travis_def = discovered.definitions.travis_ci.unwrap(); + let travis_def = discovered + .definitions + .get_first(&TaskDefinitionType::TravisCi) + .unwrap(); assert_eq!(travis_def.status, TaskFileStatus::Parsed); // Check that all matrix jobs are found as tasks @@ -2738,7 +2876,10 @@ script: let discovered = discover_tasks(temp_dir.path()); // Check that the travis-ci status is Parsed - let travis_def = discovered.definitions.travis_ci.unwrap(); + let travis_def = discovered + .definitions + .get_first(&TaskDefinitionType::TravisCi) + .unwrap(); assert_eq!(travis_def.status, TaskFileStatus::Parsed); // Check that a default task is created @@ -2762,7 +2903,10 @@ script: let discovered = discover_tasks(temp_dir.path()); // Check that the travis-ci status is NotFound - let travis_def = discovered.definitions.travis_ci.unwrap(); + let travis_def = discovered + .definitions + .get_first(&TaskDefinitionType::TravisCi) + .unwrap(); assert_eq!(travis_def.status, TaskFileStatus::NotFound); // Check that no tasks are found @@ -2807,8 +2951,16 @@ add_custom_target(build-all assert!(!cmake_tasks.is_empty()); // Check that the CMake definition was set - assert!(discovered.definitions.cmake.is_some()); - let cmake_def = discovered.definitions.cmake.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::CMake) + .is_some() + ); + let cmake_def = discovered + .definitions + .get_first(&TaskDefinitionType::CMake) + .unwrap(); assert_eq!(cmake_def.path, cmake_path); assert_eq!(cmake_def.definition_type, TaskDefinitionType::CMake); assert!(matches!(cmake_def.status, TaskFileStatus::Parsed)); @@ -2820,7 +2972,10 @@ add_custom_target(build-all let discovered = discover_tasks(temp_dir.path()); - let cmake_def = discovered.definitions.cmake.as_ref().unwrap(); + let cmake_def = discovered + .definitions + .get_first(&TaskDefinitionType::CMake) + .unwrap(); assert_eq!(cmake_def.path, temp_dir.path().join("CMakeLists.txt")); assert_eq!(cmake_def.definition_type, TaskDefinitionType::CMake); assert!(matches!(cmake_def.status, TaskFileStatus::NotFound)); @@ -2857,8 +3012,16 @@ test: # Run tests assert_eq!(justfile_tasks.len(), 2); // Check that the Justfile definition was set - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); assert_eq!(justfile_def.path, justfile_path); assert_eq!(justfile_def.definition_type, TaskDefinitionType::Justfile); assert!(matches!(justfile_def.status, TaskFileStatus::Parsed)); @@ -2896,8 +3059,16 @@ test: # Run tests assert_eq!(justfile_tasks.len(), 2); // Check that the Justfile definition was set - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); // The path should match what was actually found by the discovery function // On case-insensitive filesystems, this will be "Justfile" // On case-sensitive filesystems, this will be "justfile" @@ -2936,8 +3107,16 @@ test: # Run tests assert_eq!(justfile_tasks.len(), 2); // Check that the Justfile definition was set - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); // Should find the dot justfile since Justfile and justfile don't exist in this directory assert_eq!(justfile_def.path, justfile_dot_path); assert_eq!(justfile_def.definition_type, TaskDefinitionType::Justfile); @@ -2973,8 +3152,16 @@ build: # Build the project assert!(!discovered.tasks.is_empty()); // Should prioritize "Justfile" over others - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); assert_eq!(justfile_def.path, justfile_path); assert!(matches!(justfile_def.status, TaskFileStatus::Parsed)); @@ -2989,8 +3176,16 @@ build: # Build the project let discovered = discover_tasks(dir2); assert!(!discovered.tasks.is_empty()); - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); // The path should match what was actually found by the discovery function // On case-insensitive filesystems, this will be "Justfile" // On case-sensitive filesystems, this will be "justfile" @@ -3010,8 +3205,16 @@ build: # Build the project let discovered = discover_tasks(dir3); assert!(!discovered.tasks.is_empty()); - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); assert_eq!(justfile_def.path, justfile_dot_path); assert!(matches!(justfile_def.status, TaskFileStatus::Parsed)); @@ -3022,8 +3225,16 @@ build: # Build the project let discovered = discover_tasks(dir4); assert!(discovered.tasks.is_empty()); // No justfile should be found - assert!(discovered.definitions.justfile.is_some()); - let justfile_def = discovered.definitions.justfile.as_ref().unwrap(); + assert!( + discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .is_some() + ); + let justfile_def = discovered + .definitions + .get_first(&TaskDefinitionType::Justfile) + .unwrap(); assert_eq!(justfile_def.path, dir4.join("Justfile")); // Should use default path assert!(matches!(justfile_def.status, TaskFileStatus::NotFound)); } diff --git a/src/task_discovery/docker_compose.rs b/src/task_discovery/docker_compose.rs index 9ddf5d5..70efef0 100644 --- a/src/task_discovery/docker_compose.rs +++ b/src/task_discovery/docker_compose.rs @@ -19,7 +19,7 @@ fn discover_docker_compose_tasks( let docker_compose_files = parse_docker_compose::find_docker_compose_files(dir); if docker_compose_files.is_empty() { - discovered.definitions.docker_compose = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: dir.join("docker-compose.yml"), definition_type: TaskDefinitionType::DockerCompose, status: TaskFileStatus::NotFound, diff --git a/src/task_discovery/github_actions.rs b/src/task_discovery/github_actions.rs index bf735b1..5d16de3 100644 --- a/src/task_discovery/github_actions.rs +++ b/src/task_discovery/github_actions.rs @@ -104,7 +104,7 @@ fn discover_github_actions_tasks( } if !all_tasks.is_empty() { - discovered.definitions.github_actions = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: workflows_parent, definition_type: TaskDefinitionType::GitHubActions, status: TaskFileStatus::Parsed, diff --git a/src/task_discovery/gradle.rs b/src/task_discovery/gradle.rs index 573086b..af1c42c 100644 --- a/src/task_discovery/gradle.rs +++ b/src/task_discovery/gradle.rs @@ -61,7 +61,7 @@ fn discover_gradle_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> anyhow }; } - discovered.definitions.gradle = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: build_gradle_path, definition_type: TaskDefinitionType::Gradle, status: TaskFileStatus::NotFound, diff --git a/src/task_discovery/justfile.rs b/src/task_discovery/justfile.rs index 2218b14..263ca17 100644 --- a/src/task_discovery/justfile.rs +++ b/src/task_discovery/justfile.rs @@ -40,7 +40,7 @@ fn discover_justfile_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> anyh } } } else { - discovered.definitions.justfile = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: default_path, definition_type: TaskDefinitionType::Justfile, status: TaskFileStatus::NotFound, diff --git a/src/task_discovery/npm.rs b/src/task_discovery/npm.rs index 6747c8c..996d394 100644 --- a/src/task_discovery/npm.rs +++ b/src/task_discovery/npm.rs @@ -16,7 +16,7 @@ fn discover_npm_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> anyhow::R let package_json = dir.join("package.json"); if !package_json.exists() { - discovered.definitions.package_json = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: package_json, definition_type: TaskDefinitionType::PackageJson, status: TaskFileStatus::NotFound, diff --git a/src/task_discovery/python.rs b/src/task_discovery/python.rs index 0bd55b9..47fd240 100644 --- a/src/task_discovery/python.rs +++ b/src/task_discovery/python.rs @@ -16,7 +16,7 @@ fn discover_python_tasks(dir: &Path, discovered: &mut DiscoveredTasks) -> anyhow let pyproject_toml = dir.join("pyproject.toml"); if !pyproject_toml.exists() { - discovered.definitions.pyproject_toml = Some(TaskDefinitionFile { + discovered.definitions.insert(TaskDefinitionFile { path: pyproject_toml, definition_type: TaskDefinitionType::PyprojectToml, status: TaskFileStatus::NotFound, diff --git a/src/task_discovery/support.rs b/src/task_discovery/support.rs index 82299bc..1c4f389 100644 --- a/src/task_discovery/support.rs +++ b/src/task_discovery/support.rs @@ -10,27 +10,7 @@ pub(crate) fn apply_shadowing(tasks: &mut [Task]) { } pub(crate) 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::TurboJson => discovered.definitions.turbo_json = 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), - TaskDefinitionType::Justfile => discovered.definitions.justfile = Some(definition), - _ => {} - } + discovered.definitions.insert(definition); } pub(crate) fn handle_discovery_error( diff --git a/src/types.rs b/src/types.rs index a36747a..aa026d3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,7 +11,7 @@ pub enum ShadowType { } /// Different types of task definition files supported by dela -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum TaskDefinitionType { /// Makefile Makefile, @@ -130,29 +130,35 @@ pub struct TaskDefinitionFile { } /// Collection of discovered task definition files -#[derive(Debug, Default)] -#[allow(dead_code)] +#[derive(Debug, Clone, Default)] pub struct DiscoveredTaskDefinitions { - /// Makefile if found - pub makefile: Option, - /// package.json if found - pub package_json: Option, - /// pyproject.toml if found - pub pyproject_toml: Option, - /// Taskfile.yml if found - pub taskfile: Option, - /// turbo.json if found - pub turbo_json: Option, - /// Maven pom.xml if found - pub maven_pom: Option, - /// Gradle build files (build.gradle, build.gradle.kts) if found - pub gradle: Option, - /// GitHub Actions workflow files if found - pub github_actions: Option, - /// Docker Compose files if found - pub docker_compose: Option, - /// Justfile if found - pub justfile: Option, + files: std::collections::BTreeMap>, +} + +impl DiscoveredTaskDefinitions { + pub fn insert(&mut self, definition: TaskDefinitionFile) { + self.files + .entry(definition.definition_type.clone()) + .or_default() + .push(definition); + } + + /// Returns the first definition file for a given type, if any. + #[allow(dead_code)] + pub fn get_first(&self, definition_type: &TaskDefinitionType) -> Option<&TaskDefinitionFile> { + self.files.get(definition_type).and_then(|v| v.first()) + } + + /// Returns all definition files for a given type, if any. + #[allow(dead_code)] + pub fn get_all(&self, definition_type: &TaskDefinitionType) -> Option<&[TaskDefinitionFile]> { + self.files.get(definition_type).map(|v| v.as_slice()) + } + + /// Iterates over all (type, files) entries. + pub fn iter(&self) -> impl Iterator { + self.files.iter().map(|(k, v)| (k, v.as_slice())) + } } /// Represents a discovered task that can be executed @@ -263,18 +269,6 @@ impl TaskRunner { } } -/// Result of task discovery in a directory -#[derive(Debug, Default)] -#[allow(dead_code)] -pub struct DiscoveredTasks { - /// All tasks found, grouped by name - pub tasks: Vec, - /// Any errors encountered during discovery - pub errors: Vec, - /// Information about discovered task definition files - pub definitions: DiscoveredTaskDefinitions, -} - /// Represents the scope of user approval #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum AllowScope { @@ -326,3 +320,54 @@ pub struct Allowlist { #[serde(default)] pub entries: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_discovered_task_definitions_get_all() { + let mut defs = DiscoveredTaskDefinitions::default(); + + // 1. Assert get_all returns None on empty + assert!(defs.get_all(&TaskDefinitionType::Makefile).is_none()); + + // 2. Add multiple definitions sharing the same key, and some unique ones + let makefile1 = TaskDefinitionFile { + path: PathBuf::from("path/to/Makefile1"), + definition_type: TaskDefinitionType::Makefile, + status: TaskFileStatus::Parsed, + }; + let makefile2 = TaskDefinitionFile { + path: PathBuf::from("path/to/Makefile2"), + definition_type: TaskDefinitionType::Makefile, + status: TaskFileStatus::NotFound, + }; + let package_json = TaskDefinitionFile { + path: PathBuf::from("path/to/package.json"), + definition_type: TaskDefinitionType::PackageJson, + status: TaskFileStatus::NotImplemented, + }; + + defs.insert(makefile1.clone()); + defs.insert(makefile2.clone()); + defs.insert(package_json.clone()); + + // 3. Assert get_all returns the multiple files under the same key + let makefiles = defs.get_all(&TaskDefinitionType::Makefile).unwrap(); + assert_eq!(makefiles.len(), 2); + assert_eq!(makefiles[0].path, makefile1.path); + assert_eq!(makefiles[0].status, makefile1.status); + assert_eq!(makefiles[1].path, makefile2.path); + assert_eq!(makefiles[1].status, makefile2.status); + + // 4. Assert get_all returns the single file under its key + let package_jsons = defs.get_all(&TaskDefinitionType::PackageJson).unwrap(); + assert_eq!(package_jsons.len(), 1); + assert_eq!(package_jsons[0].path, package_json.path); + assert_eq!(package_jsons[0].status, package_json.status); + + // 5. Assert get_all returns None for query on non-inserted key + assert!(defs.get_all(&TaskDefinitionType::PyprojectToml).is_none()); + } +}