Skip to content

Commit e813fe2

Browse files
committed
Merge #94: Add ProgressReporter for multi-step command operations
7112dfb style: apply cargo fmt formatting (copilot-swe-agent[bot]) efe20cc feat: integrate ProgressReporter with create and destroy commands (copilot-swe-agent[bot]) 7705383 feat: add ProgressReporter for long-running operations (copilot-swe-agent[bot]) 90ae761 Initial plan (copilot-swe-agent[bot]) Pull request description: Commands previously provided no progress feedback during long-running operations, leaving users unable to distinguish between stuck and actively processing operations. ## Changes ### Core Implementation - **`ProgressReporter`** (`src/presentation/progress.rs`): New abstraction for standardized progress tracking with step numbering, timing, and sub-step support. Wraps `UserOutput` and maintains step state. - **`CommandContext.into_output()`**: Enables consuming context to extract `UserOutput` for ownership transfer to `ProgressReporter`. ### Command Integration - **Create command**: 3-step progression (load config → initialize → create environment) with timing per step - **Destroy command**: 3-step progression (validate → initialize → tear down) with timing per step ## Output Format ``` ⏳ [1/3] Loading configuration... ✓ Configuration loaded: demo-env (took 150ms) ⏳ [2/3] Initializing dependencies... ✓ Done (took 23ms) ⏳ [3/3] Creating environment... → Creating virtual machine → Configuring network ✓ Instance created: vm-demo-env (took 2.3s) ✅ Environment 'demo-env' created successfully ``` Duration formatting: `<1s` shown as milliseconds, `≥1s` shown as seconds with one decimal place. ## Testing 12 unit tests for `ProgressReporter` covering step tracking, timing, sub-steps, and verbosity handling. All existing command tests (15 total) updated and passing. <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>Add Progress Reporter for Long-Running Operations</issue_title> <issue_description>**Parent Issue**: #63 **Type**: 🚀 Advanced Pattern **Impact**: 🟢🟢 Medium **Effort**: 🔵🔵🔵 High **Priority**: P2 ## Problem Commands currently have no mechanism to report progress during long-running operations: - Users see no feedback during lengthy operations - No indication of current step or overall progress - Hard to know if the operation is stuck or progressing - Poor user experience for operations that take minutes Current behavior: ```rust output.progress("Creating environment..."); // Long operation happens with no updates // User sees nothing until completion output.success("Environment created!"); ``` ## Proposed Solution Create a `ProgressReporter` abstraction for standardized progress reporting: ```rust //! Progress Reporting for Long-Running Operations use std::time::{Duration, Instant}; /// Progress reporter for multi-step operations pub struct ProgressReporter { output: UserOutput, total_steps: usize, current_step: usize, step_start: Option<Instant>, } impl ProgressReporter { pub fn new(output: UserOutput, total_steps: usize) -> Self { Self { output, total_steps, current_step: 0, step_start: None, } } /// Start a new step with a description pub fn start_step(&mut self, description: &str) { self.current_step += 1; self.step_start = Some(Instant::now()); self.output.progress(&format!( "[{}/{}] {}...", self.current_step, self.total_steps, description )); } /// Complete the current step with optional result message pub fn complete_step(&mut self, result: Option<&str>) { if let Some(start) = self.step_start { let duration = start.elapsed(); if let Some(msg) = result { self.output.result(&format!( " ✓ {} (took {:?})", msg, duration )); } else { self.output.result(&format!(" ✓ Done (took {:?})", duration)); } } self.step_start = None; } /// Report a sub-step within the current step pub fn sub_step(&mut self, description: &str) { self.output.info(&format!(" → {}", description)); } /// Complete all steps and show summary pub fn complete(&mut self, summary: &str) { self.output.success(summary); } } ``` Then use in command handlers: ```rust pub fn handle_environment_creation(env_file: &Path, working_dir: &Path) -> Result<(), CreateSubcommandError> { let output = output::create_default(); let mut progress = ProgressReporter::new(output, 5); // Step 1: Load configuration progress.start_step("Loading configuration"); let config = load_configuration(env_file)?; progress.complete_step(Some(&format!("Configuration loaded: {}", config.environment.name))); // Step 2: Create dependencies progress.start_step("Initializing dependencies"); let context = CommandContext::new(working_dir.to_path_buf()); progress.complete_step(None); // Step 3: Validate environment progress.start_step("Validating environment"); validate_environment(&config)?; progress.complete_step(Some("Environment is valid")); // Step 4: Provision infrastructure progress.start_step("Provisioning infrastructure"); progress.sub_step("Creating virtual machine"); progress.sub_step("Configuring network"); let instance = provision_infrastructure(&config)?; progress.complete_step(Some(&format!("Instance created: {}", instance.name))); // Step 5: Finalize progress.start_step("Finalizing environment"); let environment = finalize_environment(config, instance)?; progress.complete_step(None); progress.complete(&format!( "Environment '{}' created successfully", environment.name() )); Ok(()) } ``` ## Benefits - ✅ Clear progress feedback for users - ✅ Shows current step and overall progress - ✅ Reports timing for each step - ✅ Consistent progress reporting across commands - ✅ Better user experience for long operations - ✅ Helps identify slow steps ## Implementation Checklist **Phase 1: Create progress reporter** - [ ] Create `src/presentation/progress.rs` - [ ] Implement `ProgressReporter` struct - [ ] Add `new()` constructor - [ ] Add `start_step()` method - [ ] Add `complete_step()` method - [ ] Add `sub_step()` method - [ ] Add `complete()` method - [ ] Add comprehensive documentation - [ ] Write unit tests **Phase 2: Integrate with create command** - [ ] Identify steps in environment creation - [ ] Add progress reporting to each step - [ ] Test with real operations - [ ... </details> - Fixes #73 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). ACKs for top commit: josecelano: ACK 7112dfb Tree-SHA512: 3283d6bcb5a135d09cd2c7ac2617d90fbc8e483ae48af03b751afe4805cfa0b15427dfb8d6d2d2ebd5a7afe789db74f3b09e37b2d851e0428feadd74e297d178
2 parents 0aba902 + 7112dfb commit e813fe2

5 files changed

Lines changed: 596 additions & 28 deletions

File tree

src/presentation/commands/context.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,26 @@ impl CommandContext {
254254
pub fn output(&mut self) -> &mut UserOutput {
255255
&mut self.output
256256
}
257+
258+
/// Consume the context and return the user output
259+
///
260+
/// This method is useful when you want to pass ownership of the output
261+
/// to another component, such as a `ProgressReporter`.
262+
///
263+
/// # Examples
264+
///
265+
/// ```rust
266+
/// use std::path::PathBuf;
267+
/// use torrust_tracker_deployer_lib::presentation::commands::context::CommandContext;
268+
/// use torrust_tracker_deployer_lib::presentation::progress::ProgressReporter;
269+
///
270+
/// let ctx = CommandContext::new(PathBuf::from("."));
271+
/// let progress = ProgressReporter::new(ctx.into_output(), 3);
272+
/// ```
273+
#[must_use]
274+
pub fn into_output(self) -> UserOutput {
275+
self.output
276+
}
257277
}
258278

259279
/// Report an error through user output

src/presentation/commands/create/subcommands/environment.rs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ use crate::application::command_handlers::create::config::EnvironmentCreationCon
99
use crate::domain::Environment;
1010
use crate::presentation::commands::context::report_error;
1111
use crate::presentation::commands::factory::CommandHandlerFactory;
12+
use crate::presentation::progress::ProgressReporter;
1213
use crate::presentation::user_output::UserOutput;
1314

1415
use super::super::config_loader::ConfigLoader;
1516
use super::super::errors::CreateSubcommandError;
1617

1718
/// Handle environment creation from configuration file
1819
///
19-
/// This function orchestrates the environment creation workflow by delegating
20-
/// to focused step functions:
20+
/// This function orchestrates the environment creation workflow with progress reporting:
2121
///
2222
/// 1. Load configuration from file
23-
/// 2. Execute create command
24-
/// 3. Display creation results
23+
/// 2. Initialize dependencies
24+
/// 3. Validate environment
25+
/// 4. Execute create command
26+
/// 5. Display creation results
2527
///
26-
/// Each step is implemented as a separate function for clarity and testability.
28+
/// Each step is tracked and timed using `ProgressReporter` for clear user feedback.
2729
///
2830
/// # Arguments
2931
///
@@ -47,17 +49,41 @@ pub fn handle_environment_creation(
4749
working_dir: &Path,
4850
) -> Result<(), CreateSubcommandError> {
4951
let factory = CommandHandlerFactory::new();
50-
let mut ctx = factory.create_context(working_dir.to_path_buf());
52+
let ctx = factory.create_context(working_dir.to_path_buf());
53+
54+
// Create progress reporter for 3 main steps
55+
let mut progress = ProgressReporter::new(ctx.into_output(), 3);
5156

5257
// Step 1: Load configuration
53-
let config = load_configuration(ctx.output(), env_file)?;
58+
progress.start_step("Loading configuration");
59+
let config = load_configuration(progress.output(), env_file)?;
60+
progress.complete_step(Some(&format!(
61+
"Configuration loaded: {}",
62+
config.environment.name
63+
)));
5464

55-
// Step 2: Execute command (create handler before borrowing output)
65+
// Step 2: Initialize dependencies
66+
progress.start_step("Initializing dependencies");
67+
let ctx = factory.create_context(working_dir.to_path_buf());
5668
let command_handler = factory.create_create_handler(&ctx);
57-
let environment = execute_create_command(ctx.output(), &command_handler, config)?;
69+
progress.complete_step(None);
70+
71+
// Step 3: Execute create command (provision infrastructure)
72+
progress.start_step("Creating environment");
73+
let environment = execute_create_command(progress.output(), &command_handler, config)?;
74+
progress.complete_step(Some(&format!(
75+
"Instance created: {}",
76+
environment.instance_name().as_str()
77+
)));
78+
79+
// Complete with summary
80+
progress.complete(&format!(
81+
"Environment '{}' created successfully",
82+
environment.name().as_str()
83+
));
5884

59-
// Step 3: Display results
60-
display_creation_results(ctx.output(), &environment);
85+
// Display final results
86+
display_creation_results(progress.output(), &environment);
6187

6288
Ok(())
6389
}

src/presentation/commands/destroy/handler.rs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
use crate::domain::environment::name::EnvironmentName;
77
use crate::presentation::commands::context::report_error;
88
use crate::presentation::commands::factory::CommandHandlerFactory;
9+
use crate::presentation::progress::ProgressReporter;
910

1011
use super::errors::DestroySubcommandError;
1112

1213
/// Handle the destroy command
1314
///
14-
/// This function orchestrates the environment destruction workflow by:
15+
/// This function orchestrates the environment destruction workflow with progress reporting:
1516
/// 1. Validating the environment name
16-
/// 2. Loading the environment from persistent storage
17-
/// 3. Executing the destroy command handler
18-
/// 4. Providing user-friendly progress updates
17+
/// 2. Tearing down infrastructure
18+
/// 3. Cleaning up resources
19+
///
20+
/// Each step is tracked and timed using `ProgressReporter` for clear user feedback.
1921
///
2022
/// # Arguments
2123
///
@@ -53,39 +55,45 @@ pub fn handle_destroy_command(
5355
) -> Result<(), DestroySubcommandError> {
5456
// Create factory and context with all shared dependencies
5557
let factory = CommandHandlerFactory::new();
56-
let mut ctx = factory.create_context(working_dir.to_path_buf());
58+
let ctx = factory.create_context(working_dir.to_path_buf());
5759

58-
// Display initial progress (to stderr)
59-
ctx.output()
60-
.progress(&format!("Destroying environment '{environment_name}'..."));
60+
// Create progress reporter for 3 main steps
61+
let mut progress = ProgressReporter::new(ctx.into_output(), 3);
6162

62-
// Validate environment name
63+
// Step 1: Validate environment name
64+
progress.start_step("Validating environment");
6365
let env_name = EnvironmentName::new(environment_name.to_string()).map_err(|source| {
6466
let error = DestroySubcommandError::InvalidEnvironmentName {
6567
name: environment_name.to_string(),
6668
source,
6769
};
68-
report_error(ctx.output(), &error);
70+
report_error(progress.output(), &error);
6971
error
7072
})?;
73+
progress.complete_step(Some(&format!(
74+
"Environment name validated: {environment_name}"
75+
)));
7176

72-
// Create and execute destroy command handler
73-
ctx.output().progress("Tearing down infrastructure...");
74-
77+
// Step 2: Initialize dependencies
78+
progress.start_step("Initializing dependencies");
79+
let ctx = factory.create_context(working_dir.to_path_buf());
7580
let command_handler = factory.create_destroy_handler(&ctx);
81+
progress.complete_step(None);
7682

77-
// Execute destroy - the handler will load the environment and handle all states internally
83+
// Step 3: Execute destroy command (tear down infrastructure)
84+
progress.start_step("Tearing down infrastructure");
7885
let _destroyed_env = command_handler.execute(&env_name).map_err(|source| {
7986
let error = DestroySubcommandError::DestroyOperationFailed {
8087
name: environment_name.to_string(),
8188
source,
8289
};
83-
report_error(ctx.output(), &error);
90+
report_error(progress.output(), &error);
8491
error
8592
})?;
93+
progress.complete_step(Some("Infrastructure torn down"));
8694

87-
ctx.output().progress("Cleaning up resources...");
88-
ctx.output().success(&format!(
95+
// Complete with summary
96+
progress.complete(&format!(
8997
"Environment '{environment_name}' destroyed successfully"
9098
));
9199

src/presentation/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
pub mod cli;
4242
pub mod commands;
4343
pub mod errors;
44+
pub mod progress;
4445
pub mod user_output;
4546

4647
// Re-export commonly used presentation types for convenience
@@ -49,4 +50,5 @@ pub use commands::create::CreateSubcommandError;
4950
pub use commands::destroy::DestroySubcommandError;
5051
pub use commands::{execute, handle_error};
5152
pub use errors::CommandError;
53+
pub use progress::ProgressReporter;
5254
pub use user_output::{UserOutput, VerbosityLevel};

0 commit comments

Comments
 (0)