diff --git a/src/analyze/cli.rs b/src/analyze/cli.rs index 25345a6..b677862 100644 --- a/src/analyze/cli.rs +++ b/src/analyze/cli.rs @@ -1,4 +1,5 @@ use super::error::AnalyzeError; +use crate::cli::InquireInteraction; use crate::prelude::{ CaptureError, CaptureOpts, DefaultExecutionProvider, ExecutionProvider, OutputDestination, }; @@ -50,12 +51,14 @@ pub async fn analyze_root(found_config: &FoundConfig, args: &AnalyzeArgs) -> Res } async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Result { + let interaction = InquireInteraction; let result = match args.location.as_str() { "-" => { analyze::process_lines( &found_config.known_error, &found_config.working_dir, read_from_stdin().await?, + &interaction, ) .await? } @@ -64,6 +67,7 @@ async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Res &found_config.known_error, &found_config.working_dir, read_from_file(file_path).await?, + &interaction, ) .await? } @@ -75,6 +79,7 @@ async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Res async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs) -> Result { let exec_runner = DefaultExecutionProvider::default(); + let interaction = InquireInteraction; let command = args.command.clone(); let path = env::var("PATH").unwrap_or_default(); @@ -91,6 +96,7 @@ async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs) &found_config.known_error, &found_config.working_dir, read_from_command(&exec_runner, capture_opts).await?, + &interaction, ) .await?; diff --git a/src/analyze/options.rs b/src/analyze/options.rs new file mode 100644 index 0000000..62b1a61 --- /dev/null +++ b/src/analyze/options.rs @@ -0,0 +1,148 @@ +//! CLI-independent options types for the analyze module. +//! +//! This module provides options types that can be used to configure +//! analyze operations without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! The analyze functionality detects known errors in command output or log files +//! and can optionally run fixes for detected errors. +//! +//! # Examples +//! +//! ## Basic Configuration +//! +//! ```rust +//! use dx_scope::AnalyzeOptions; +//! use std::collections::BTreeMap; +//! use std::path::PathBuf; +//! +//! let options = AnalyzeOptions { +//! known_errors: BTreeMap::new(), +//! working_dir: PathBuf::from("/path/to/project"), +//! }; +//! assert!(options.known_errors.is_empty()); +//! ``` +//! +//! ## Different Input Sources +//! +//! ```rust,no_run +//! use dx_scope::analyze::options::AnalyzeInput; +//! +//! // From a file +//! let input = AnalyzeInput::from_file("/var/log/build.log"); +//! +//! // From stdin +//! let input = AnalyzeInput::Stdin; +//! +//! // From in-memory lines (useful for testing or library usage) +//! let input = AnalyzeInput::from_lines(vec![ +//! "Building project...".to_string(), +//! "error: missing dependency".to_string(), +//! ]); +//! ``` + +use crate::shared::prelude::KnownError; +use std::collections::BTreeMap; +use std::path::PathBuf; + +/// Options for running analyze operations. +/// +/// This struct contains all the configuration needed to run analysis, +/// without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust +/// use dx_scope::AnalyzeOptions; +/// use std::collections::BTreeMap; +/// use std::path::PathBuf; +/// +/// let options = AnalyzeOptions { +/// known_errors: BTreeMap::new(), +/// working_dir: PathBuf::from("/path/to/project"), +/// }; +/// assert!(options.known_errors.is_empty()); +/// ``` +#[derive(Debug, Clone)] +pub struct AnalyzeOptions { + /// Map of known errors to detect and potentially fix + pub known_errors: BTreeMap, + /// Working directory for running fix commands + pub working_dir: PathBuf, +} + +impl AnalyzeOptions { + /// Create new analyze options with the given parameters. + pub fn new(known_errors: BTreeMap, working_dir: PathBuf) -> Self { + Self { + known_errors, + working_dir, + } + } +} + +impl Default for AnalyzeOptions { + fn default() -> Self { + Self { + known_errors: BTreeMap::new(), + working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + } + } +} + +/// Specifies the input source for analysis. +/// +/// The analyze functionality can process input from various sources. +/// This enum allows callers to specify where the input should come from. +#[derive(Debug, Clone)] +pub enum AnalyzeInput { + /// Read from a file at the given path + File(PathBuf), + /// Read from standard input + Stdin, + /// Process the given lines directly (useful for library usage) + Lines(Vec), +} + +impl AnalyzeInput { + /// Create input from a file path. + pub fn from_file(path: impl Into) -> Self { + Self::File(path.into()) + } + + /// Create input from a vector of strings. + pub fn from_lines(lines: Vec) -> Self { + Self::Lines(lines) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_analyze_options_default() { + let options = AnalyzeOptions::default(); + assert!(options.known_errors.is_empty()); + } + + #[test] + fn test_analyze_input_from_file() { + let input = AnalyzeInput::from_file("/path/to/file"); + match input { + AnalyzeInput::File(path) => assert_eq!(path, PathBuf::from("/path/to/file")), + _ => panic!("Expected File variant"), + } + } + + #[test] + fn test_analyze_input_from_lines() { + let lines = vec!["line1".to_string(), "line2".to_string()]; + let input = AnalyzeInput::from_lines(lines.clone()); + match input { + AnalyzeInput::Lines(l) => assert_eq!(l, lines), + _ => panic!("Expected Lines variant"), + } + } +} diff --git a/src/bin/scope.rs b/src/bin/scope.rs index e2db22e..7d4954e 100644 --- a/src/bin/scope.rs +++ b/src/bin/scope.rs @@ -4,6 +4,7 @@ use clap::{Parser, Subcommand}; use colored::Colorize; use dx_scope::prelude::*; use dx_scope::report_stdout; +use dx_scope::shared::print_details; use human_panic::setup_panic; use lazy_static::lazy_static; use regex::Regex; diff --git a/src/doctor/check.rs b/src/doctor/check.rs index c4ca290..175e6a9 100644 --- a/src/doctor/check.rs +++ b/src/doctor/check.rs @@ -6,6 +6,7 @@ use std::{cmp, vec}; use tokio::io::BufReader; use tracing::debug; +use crate::internal::prompts::AutoApprove; use crate::models::HelpMetadata; use crate::prelude::{ ActionReport, ActionReportBuilder, ActionTaskReport, DoctorCommand, KnownError, @@ -672,7 +673,9 @@ impl DefaultDoctorActionRun { .collect::>(); let buffer = BufReader::new(StringVecReader::new(&lines)); - analyze::process_lines(&self.known_errors, &self.working_dir, buffer).await + // Use AutoApprove for self-healing: known errors during doctor runs + // are automatically fixed without user prompts + analyze::process_lines(&self.known_errors, &self.working_dir, buffer, &AutoApprove).await } } diff --git a/src/doctor/options.rs b/src/doctor/options.rs new file mode 100644 index 0000000..0c5b05b --- /dev/null +++ b/src/doctor/options.rs @@ -0,0 +1,152 @@ +//! CLI-independent options types for the doctor module. +//! +//! This module provides options types that can be used to configure +//! doctor operations without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! The doctor functionality runs health checks on your development environment +//! and can automatically apply fixes when issues are detected. +//! +//! # Examples +//! +//! ## Common Configurations +//! +//! ```rust +//! use dx_scope::doctor::options::DoctorRunOptions; +//! +//! // CI mode - run checks without fixes +//! let ci_options = DoctorRunOptions::ci_mode(); +//! +//! // Interactive mode - prompt for fixes +//! let interactive_options = DoctorRunOptions::with_fixes(); +//! +//! // Run specific groups only +//! let targeted_options = DoctorRunOptions::for_groups(vec![ +//! "rust".to_string(), +//! "docker".to_string(), +//! ]); +//! ``` +//! +//! ## Full Customization +//! +//! ```rust +//! use dx_scope::doctor::options::DoctorRunOptions; +//! use std::path::PathBuf; +//! +//! let options = DoctorRunOptions { +//! only_groups: Some(vec!["build".to_string()]), +//! run_fix: true, +//! cache_dir: Some(PathBuf::from("/tmp/my-cache")), +//! no_cache: false, +//! auto_publish_report: true, +//! }; +//! ``` +//! +//! # Caching +//! +//! The doctor module supports caching to avoid re-running checks when +//! source files haven't changed. Control caching with: +//! +//! - `cache_dir`: Custom cache location (default: system cache directory) +//! - `no_cache`: Disable caching entirely (useful for debugging) + +use std::path::PathBuf; + +/// Options for running doctor operations. +/// +/// This struct contains all the configuration needed to run doctor checks, +/// without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust,no_run +/// use dx_scope::DoctorRunOptions; +/// +/// // Run all default groups with fixes enabled +/// let options = DoctorRunOptions::default(); +/// +/// // Run only specific groups +/// let options = DoctorRunOptions { +/// only_groups: Some(vec!["build".to_string(), "test".to_string()]), +/// ..Default::default() +/// }; +/// +/// // Run in CI mode (no interactive fixes) +/// let options = DoctorRunOptions::ci_mode(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct DoctorRunOptions { + /// Run only these groups (None = run all default groups) + pub only_groups: Option>, + /// Whether to run fixes when checks fail + pub run_fix: bool, + /// Custom cache directory path + pub cache_dir: Option, + /// Disable caching + pub no_cache: bool, + /// Automatically publish report on failure + pub auto_publish_report: bool, +} + +impl DoctorRunOptions { + /// Create new options with default values but fixes enabled. + pub fn with_fixes() -> Self { + Self { + run_fix: true, + ..Default::default() + } + } + + /// Create new options for CI/non-interactive mode (no fixes). + pub fn ci_mode() -> Self { + Self { + run_fix: false, + ..Default::default() + } + } + + /// Create options to run specific groups only. + pub fn for_groups(groups: Vec) -> Self { + Self { + only_groups: Some(groups), + run_fix: true, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_options() { + let options = DoctorRunOptions::default(); + assert!(options.only_groups.is_none()); + assert!(!options.run_fix); + assert!(options.cache_dir.is_none()); + assert!(!options.no_cache); + assert!(!options.auto_publish_report); + } + + #[test] + fn test_with_fixes() { + let options = DoctorRunOptions::with_fixes(); + assert!(options.run_fix); + } + + #[test] + fn test_ci_mode() { + let options = DoctorRunOptions::ci_mode(); + assert!(!options.run_fix); + } + + #[test] + fn test_for_groups() { + let groups = vec!["group1".to_string(), "group2".to_string()]; + let options = DoctorRunOptions::for_groups(groups.clone()); + assert_eq!(options.only_groups, Some(groups)); + assert!(options.run_fix); + } +} diff --git a/src/shared/analyze/mod.rs b/src/shared/analyze/mod.rs index 2d86f5b..ec8f77f 100644 --- a/src/shared/analyze/mod.rs +++ b/src/shared/analyze/mod.rs @@ -1,10 +1,10 @@ +use crate::internal::prompts::UserInteraction; use crate::models::HelpMetadata; use crate::prelude::{ CaptureOpts, DefaultExecutionProvider, DoctorFix, ExecutionProvider, KnownError, OutputCapture, OutputDestination, generate_env_vars, }; use anyhow::Result; -use inquire::InquireError; use std::collections::BTreeMap; use std::path::PathBuf; use tokio::io::{AsyncBufReadExt, AsyncRead}; @@ -13,15 +13,28 @@ use tracing::{debug, error, info, warn}; mod status; pub use crate::shared::analyze::status::{AnalyzeStatus, report_result}; -pub async fn process_lines( +/// Process lines of input to detect known errors and optionally run fixes. +/// +/// This function scans the input for known error patterns. When a match is found, +/// it will prompt the user (via the `UserInteraction` trait) to run the associated fix. +/// +/// # Arguments +/// * `known_errors` - Map of known errors to detect +/// * `working_dir` - Directory to run fix commands in +/// * `input` - Async reader providing the lines to analyze +/// * `user_interaction` - Implementation of `UserInteraction` for prompting +/// +/// # Returns +/// `AnalyzeStatus` indicating the outcome of the analysis +pub async fn process_lines( known_errors: &BTreeMap, working_dir: &PathBuf, input: T, + user_interaction: &U, ) -> Result where - T: AsyncRead, - T: AsyncBufReadExt, - T: Unpin, + T: AsyncRead + AsyncBufReadExt + Unpin, + U: UserInteraction, { let mut result = AnalyzeStatus::NoKnownErrorsFound; let mut known_errors = known_errors.clone(); @@ -41,11 +54,8 @@ where Some(fix) => { info!(target: "always", "found a fix!"); - tracing_indicatif::suspend_tracing_indicatif(|| { - let exec_path = ke.metadata.exec_path(); - prompt_and_run_fix(working_dir, exec_path, fix) - }) - .await? + let exec_path = ke.metadata.exec_path(); + prompt_and_run_fix(working_dir, exec_path, fix, user_interaction).await? } None => AnalyzeStatus::KnownErrorFoundNoFixFound, }; @@ -69,62 +79,45 @@ where Ok(result) } -async fn prompt_and_run_fix( +async fn prompt_and_run_fix( working_dir: &PathBuf, exec_path: String, fix: &DoctorFix, + user_interaction: &U, ) -> Result { let fix_prompt = &fix.prompt.as_ref(); let prompt_text = fix_prompt .map(|p| p.text.clone()) .unwrap_or("Would you like to run it?".to_string()); - let extra_context = &fix_prompt.map(|p| p.extra_context.clone()).flatten(); - - let prompt = { - let base_prompt = inquire::Confirm::new(&prompt_text).with_default(false); - match extra_context { - Some(help_text) => base_prompt.with_help_message(help_text), - None => base_prompt, - } - }; - - match prompt.prompt() { - Ok(user_accepted) => { - if user_accepted { - // failure indicates an issue with us actually executing it, - // not the success/failure of the command itself. - let outputs = run_fix(working_dir, &exec_path, fix).await?; - let max_exit_code = outputs - .iter() - .map(|c| c.exit_code.unwrap_or(-1)) - .max() - .unwrap(); - - match max_exit_code { - 0 => Ok(AnalyzeStatus::KnownErrorFoundFixSucceeded), - _ => { - if let Some(help_text) = &fix.help_text { - error!(target: "user", "Fix Help: {}", help_text); - } - if let Some(help_url) = &fix.help_url { - error!(target: "user", "For more help, please visit {}", help_url); - } - - Ok(AnalyzeStatus::KnownErrorFoundFixFailed) - } + let extra_context = fix_prompt.and_then(|p| p.extra_context.clone()); + + let user_accepted = user_interaction.confirm(&prompt_text, extra_context.as_deref()); + + if user_accepted { + // failure indicates an issue with us actually executing it, + // not the success/failure of the command itself. + let outputs = run_fix(working_dir, &exec_path, fix).await?; + let max_exit_code = outputs + .iter() + .map(|c| c.exit_code.unwrap_or(-1)) + .max() + .unwrap(); + + match max_exit_code { + 0 => Ok(AnalyzeStatus::KnownErrorFoundFixSucceeded), + _ => { + if let Some(help_text) = &fix.help_text { + error!(target: "user", "Fix Help: {}", help_text); + } + if let Some(help_url) = &fix.help_url { + error!(target: "user", "For more help, please visit {}", help_url); } - } else { - Ok(AnalyzeStatus::KnownErrorFoundUserDenied) + + Ok(AnalyzeStatus::KnownErrorFoundFixFailed) } } - Err(InquireError::NotTTY) => { - warn!(target: "user", "Prompting user for fix, but input device is not a TTY. Skipping fix."); - Ok(AnalyzeStatus::KnownErrorFoundUserDenied) - } - Err(e) => { - error!(target: "user", "Error prompting user for fix: {}", e); - Err(e.into()) - } + } else { + Ok(AnalyzeStatus::KnownErrorFoundUserDenied) } } diff --git a/src/shared/config/mod.rs b/src/shared/config/mod.rs new file mode 100644 index 0000000..6852c4b --- /dev/null +++ b/src/shared/config/mod.rs @@ -0,0 +1,8 @@ +//! Configuration loading abstractions for library-first design. +//! +//! This module provides CLI-independent configuration loading options +//! and functions that can be used programmatically. + +mod options; + +pub use options::ConfigLoadOptions; diff --git a/src/shared/config/options.rs b/src/shared/config/options.rs new file mode 100644 index 0000000..5d7a37e --- /dev/null +++ b/src/shared/config/options.rs @@ -0,0 +1,210 @@ +//! CLI-independent configuration loading options. +//! +//! This module provides options types that can be used to configure +//! configuration loading without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! Scope configuration is loaded from YAML files in `.scope` directories. +//! By default, scope searches: +//! +//! 1. Ancestor directories from the working directory (`.scope/`) +//! 2. User home directory (`~/.scope/`) +//! 3. System config directory +//! +//! # Examples +//! +//! ## Default Discovery +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! +//! // Uses default discovery from current directory +//! let options = ConfigLoadOptions::default(); +//! ``` +//! +//! ## Custom Working Directory +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Search from a specific directory +//! let options = ConfigLoadOptions::with_working_dir( +//! PathBuf::from("/path/to/my/project") +//! ); +//! ``` +//! +//! ## Additional Config Paths +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Add extra config directories to the search +//! let options = ConfigLoadOptions::with_extra_config(vec![ +//! PathBuf::from("/shared/team/config"), +//! PathBuf::from("/company/global/scope"), +//! ]); +//! ``` +//! +//! ## Explicit Paths Only +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Disable default discovery, use only specified paths +//! let options = ConfigLoadOptions::explicit_only(vec![ +//! PathBuf::from("/my/config/only"), +//! ]); +//! ``` +//! +//! # Configuration Precedence +//! +//! When the same configuration item appears in multiple files, the first +//! occurrence wins. Files are processed in order: +//! +//! 1. Most specific directory (closest to working dir) +//! 2. Parent directories (ascending) +//! 3. Home directory +//! 4. System config +//! 5. Extra config paths (in order specified) + +use std::path::PathBuf; + +/// Options for loading scope configuration. +/// +/// This struct contains all the configuration needed to find and load +/// scope configuration files, without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust,no_run +/// use dx_scope::shared::config::ConfigLoadOptions; +/// use std::path::PathBuf; +/// +/// // Use default configuration discovery +/// let options = ConfigLoadOptions::default(); +/// +/// // Load from specific directories +/// let options = ConfigLoadOptions { +/// extra_config: vec![PathBuf::from("/custom/config/path")], +/// ..Default::default() +/// }; +/// +/// // Disable default config discovery (only use explicit paths) +/// let options = ConfigLoadOptions { +/// disable_default_config: true, +/// extra_config: vec![PathBuf::from("/my/config")], +/// ..Default::default() +/// }; +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ConfigLoadOptions { + /// Additional paths to search for configuration files. + /// These are added to the default discovery paths. + pub extra_config: Vec, + + /// When true, skip default config discovery (ancestor .scope directories, + /// home directory, system config). Only explicitly specified paths are used. + pub disable_default_config: bool, + + /// Override the working directory for config discovery and command execution. + /// If None, uses the current working directory. + pub working_dir: Option, + + /// Custom run ID for this execution. + /// If None, a unique ID will be generated. + pub run_id: Option, +} + +impl ConfigLoadOptions { + /// Create options that only use explicitly specified config paths. + pub fn explicit_only(paths: Vec) -> Self { + Self { + extra_config: paths, + disable_default_config: true, + ..Default::default() + } + } + + /// Create options with a custom working directory. + pub fn with_working_dir(working_dir: PathBuf) -> Self { + Self { + working_dir: Some(working_dir), + ..Default::default() + } + } + + /// Create options with additional config paths. + pub fn with_extra_config(paths: Vec) -> Self { + Self { + extra_config: paths, + ..Default::default() + } + } + + /// Get the working directory, falling back to current directory. + pub fn get_working_dir(&self) -> std::io::Result { + match &self.working_dir { + Some(dir) => Ok(dir.clone()), + None => std::env::current_dir(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_options() { + let options = ConfigLoadOptions::default(); + assert!(options.extra_config.is_empty()); + assert!(!options.disable_default_config); + assert!(options.working_dir.is_none()); + assert!(options.run_id.is_none()); + } + + #[test] + fn test_explicit_only() { + let paths = vec![PathBuf::from("/path1"), PathBuf::from("/path2")]; + let options = ConfigLoadOptions::explicit_only(paths.clone()); + assert_eq!(options.extra_config, paths); + assert!(options.disable_default_config); + } + + #[test] + fn test_with_working_dir() { + let dir = PathBuf::from("/custom/dir"); + let options = ConfigLoadOptions::with_working_dir(dir.clone()); + assert_eq!(options.working_dir, Some(dir)); + } + + #[test] + fn test_with_extra_config() { + let paths = vec![PathBuf::from("/extra/config")]; + let options = ConfigLoadOptions::with_extra_config(paths.clone()); + assert_eq!(options.extra_config, paths); + assert!(!options.disable_default_config); + } + + #[test] + fn test_get_working_dir_with_override() { + let dir = PathBuf::from("/override/dir"); + let options = ConfigLoadOptions { + working_dir: Some(dir.clone()), + ..Default::default() + }; + assert_eq!(options.get_working_dir().unwrap(), dir); + } + + #[test] + fn test_get_working_dir_without_override() { + let options = ConfigLoadOptions::default(); + // Should return the current directory + let result = options.get_working_dir(); + assert!(result.is_ok()); + } +} diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 7d28589..c6e4ab2 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -1,11 +1,12 @@ use colored::Colorize; +use std::cmp::max; +use std::path::Path; use crate::models::HelpMetadata; use crate::report_stdout; -use std::cmp::max; -use std::path::Path; mod capture; +pub mod config; mod config_load; mod logging; @@ -23,10 +24,10 @@ pub mod prelude { CaptureError, CaptureOpts, DefaultExecutionProvider, ExecutionProvider, MockExecutionProvider, OutputCapture, OutputCaptureBuilder, OutputDestination, }; + pub use super::config::ConfigLoadOptions; pub use super::config_load::{ConfigOptions, FoundConfig, build_config_path}; pub use super::logging::{LoggingOpts, STDERR_WRITER, STDOUT_WRITER, progress_bar_without_pos}; pub use super::models::prelude::*; - pub use super::print_details; pub use super::report::{ ActionReport, ActionReportBuilder, ActionTaskReport, ActionTaskReportBuilder, DefaultGroupedReportBuilder, DefaultUnstructuredReportBuilder, GroupReport, @@ -39,6 +40,9 @@ pub(crate) fn convert_to_string(input: Vec<&str>) -> Vec { input.iter().map(|x| x.to_string()).collect() } +/// Print details of configuration resources in a formatted table. +/// +/// This is a CLI utility function that outputs directly to stdout. pub async fn print_details(working_dir: &Path, config: &Vec) where T: HelpMetadata, diff --git a/src/shared/report.rs b/src/shared/report.rs index c492142..0e66cc9 100644 --- a/src/shared/report.rs +++ b/src/shared/report.rs @@ -575,7 +575,16 @@ pub(crate) mod tests { use anyhow::Result; use chrono::DateTime; - use crate::prelude::*; + use super::{ + ActionReport, ActionTaskReport, DefaultGroupedReportBuilder, + DefaultUnstructuredReportBuilder, GroupReport, GroupedReportBuilder, ReportRenderer, + UnstructuredReportBuilder, + }; + use crate::models::prelude::ModelMetadata; + use crate::shared::models::prelude::{ + ReportTemplates, ReportUploadLocation, ReportUploadLocationDestination, + }; + use crate::shared::prelude::{FoundConfig, MockExecutionProvider, OutputCaptureBuilder}; #[tokio::test] async fn test_grouped_report_builder() -> Result<()> {