Skip to content

Commit 7486b25

Browse files
committed
Merge #141: Add flush() method to UserOutput for explicit buffering control
b3f98c7 feat: [#138] add buffering control with flush() method (copilot-swe-agent[bot]) b537a8f Initial plan (copilot-swe-agent[bot]) Pull request description: Adds explicit buffering control to `UserOutput` following Rust's `Write` trait pattern. While output is line-buffered by default, explicit flush ensures immediate visibility before long-running operations and improves test reliability. ## Changes **Core API** - Added `flush()` method that flushes both stdout and stderr writers - Returns `std::io::Result<()>` for error propagation - Safe to call multiple times (idempotent) **Documentation** - Module-level "Buffering Behavior" section explaining when to use `flush()` - Rustdoc examples demonstrating usage patterns **Tests** - 5 tests covering success scenarios, idempotency, empty buffers, dual-channel flushing, and sequential patterns - All existing tests pass (101 total in module) ## Usage ```rust let mut output = UserOutput::new(VerbosityLevel::Normal); output.progress("Starting long operation..."); output.flush()?; // Ensure message appears immediately perform_long_operation(); ``` ## Implementation Directly flushes the typed writer wrappers (`StdoutWriter` and `StderrWriter`) maintaining type safety through the newtype pattern: ```rust pub fn flush(&mut self) -> std::io::Result<()> { self.stdout.0.flush()?; self.stderr.0.flush()?; Ok(()) } ``` Backward compatible - purely additive change. <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Proposal 7: Add Buffering Control</issue_title> > <issue_description>## Overview > > Add explicit buffering control to the `UserOutput` module by providing a `flush()` method. While writes are typically line-buffered by default, explicit flush control ensures output appears immediately when needed and provides better testing capabilities. > > ## Specification > > See detailed specification: [docs/issues/138-add-buffering-control.md](../docs/issues/138-add-buffering-control.md) > > ## Goals > > - [ ] Add `flush()` method to `UserOutput` for explicit buffer control > - [ ] Document buffering behavior in module documentation > - [ ] Add tests demonstrating flush behavior > - [ ] Maintain backward compatibility with existing API > - [ ] Follow Rust standard patterns from `Write` trait > > ## Implementation Plan > > ### Phase 1: Core Implementation (30 minutes) > - [ ] Add `flush()` method to `UserOutput` > - [ ] Implement flush for both `stdout` and `stderr` writers > - [ ] Add rustdoc documentation for the method > > ### Phase 2: Documentation (20 minutes) > - [ ] Update module-level documentation with buffering behavior section > - [ ] Add usage examples showing when to use `flush()` > > ### Phase 3: Testing (30 minutes) > - [ ] Add unit tests for successful flush > - [ ] Add tests for multiple flush calls > - [ ] Add tests for empty buffer flushing > > ### Phase 4: Quality Assurance (20 minutes) > - [ ] Run `./scripts/pre-commit.sh` and fix any issues > - [ ] Verify all linters pass > > **Total Estimated Time**: 2 hours > > ## Acceptance Criteria > > ### Functional Requirements > - [ ] `UserOutput::flush()` method flushes both stdout and stderr > - [ ] Flush can be called multiple times safely > - [ ] Flush returns `std::io::Result<()>` for error handling > - [ ] Existing functionality is not affected > > ### Testing Requirements > - [ ] Unit tests cover successful flush scenarios > - [ ] Tests verify idempotency (multiple flushes are safe) > - [ ] All existing tests continue to pass > > ### Quality Requirements > - [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` > - [ ] Code follows project conventions > > ## Related > > - **Parent Epic**: #102 - User Output Architecture Improvements > - **Refactoring Plan**: [docs/refactors/plans/user-output-architecture-improvements.md](../docs/refactors/plans/user-output-architecture-improvements.md) > - **Depends On**: #135 - Type-Safe Channel Routing > > ## Labels > > `enhancement`, `phase-2`, `user-output`, `P2` > > ## Priority > > P2 (Phase 2 - Polish & Extensions) > > ## Estimated Effort > > 2 hours > </issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > <comment_new><author>@josecelano</author><body> > **Parent Epic**: #102 - User Output Architecture Improvements > > This issue is part of Phase 2: Polish & Extensions of the User Output Architecture refactoring.</body></comment_new> > </comments> > </details> - Fixes #138 <!-- 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 b3f98c7 Tree-SHA512: 0fc84cdba38998b69432de679681c740f4f698f8ad66640fd8ba806c36c659851373097e5a2e1bdaa469cac45174c6d1702e34b9fd18abe6383c1675b16be17c
2 parents 10d1ace + b3f98c7 commit 7486b25

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

src/presentation/user_output.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
//! The newtype pattern is a zero-cost abstraction - it has the same memory layout and performance
2424
//! characteristics as the wrapped type, but provides type safety benefits.
2525
//!
26+
//! ## Buffering Behavior
27+
//!
28+
//! Output is line-buffered by default. Messages are typically flushed automatically
29+
//! after each newline. For cases where immediate output is critical (e.g., before
30+
//! long-running operations), call `flush()` explicitly:
31+
//!
32+
//! ```rust,ignore
33+
//! output.progress("Starting long operation...");
34+
//! output.flush()?; // Ensure message appears before operation starts
35+
//! perform_long_operation();
36+
//! ```
37+
//!
2638
//! ## Example Usage
2739
//!
2840
//! ```rust
@@ -1083,6 +1095,32 @@ impl UserOutput {
10831095
}
10841096
}
10851097

1098+
/// Flush all pending output to stdout and stderr
1099+
///
1100+
/// This is typically not needed as writes are line-buffered by default,
1101+
/// but can be useful for ensuring output appears immediately before
1102+
/// long-running operations or in test scenarios.
1103+
///
1104+
/// # Errors
1105+
///
1106+
/// Returns an error if flushing either stream fails.
1107+
///
1108+
/// # Examples
1109+
///
1110+
/// ```rust
1111+
/// use torrust_tracker_deployer_lib::presentation::user_output::{UserOutput, VerbosityLevel};
1112+
///
1113+
/// let mut output = UserOutput::new(VerbosityLevel::Normal);
1114+
/// output.progress("Starting long operation...");
1115+
/// output.flush().expect("Failed to flush output");
1116+
/// // Now perform long operation...
1117+
/// ```
1118+
pub fn flush(&mut self) -> std::io::Result<()> {
1119+
self.stdout.0.flush()?;
1120+
self.stderr.0.flush()?;
1121+
Ok(())
1122+
}
1123+
10861124
/// Display progress message to stderr (Normal level and above)
10871125
///
10881126
/// Progress messages go to stderr following cargo/docker patterns.
@@ -2744,4 +2782,106 @@ mod tests {
27442782
assert!(json.is_ok(), "Invalid JSON in stdout");
27452783
}
27462784
}
2785+
2786+
// ============================================================================
2787+
// Buffering Tests
2788+
// ============================================================================
2789+
2790+
mod buffering {
2791+
use super::super::*;
2792+
use crate::presentation::user_output::test_support::TestUserOutput;
2793+
2794+
#[test]
2795+
fn it_should_flush_all_writers() {
2796+
let mut test_output = TestUserOutput::new(VerbosityLevel::Normal);
2797+
test_output.output.progress("Test message");
2798+
2799+
// Flush should succeed
2800+
test_output.output.flush().expect("Flush should succeed");
2801+
2802+
// Verify output is present (flushed)
2803+
assert!(!test_output.stderr().is_empty());
2804+
assert!(test_output.stderr().contains("Test message"));
2805+
}
2806+
2807+
#[test]
2808+
fn it_should_be_safe_to_flush_multiple_times() {
2809+
let mut test_output = TestUserOutput::new(VerbosityLevel::Normal);
2810+
test_output.output.progress("Test message");
2811+
2812+
// Multiple flushes should be safe
2813+
test_output
2814+
.output
2815+
.flush()
2816+
.expect("First flush should succeed");
2817+
test_output
2818+
.output
2819+
.flush()
2820+
.expect("Second flush should succeed");
2821+
test_output
2822+
.output
2823+
.flush()
2824+
.expect("Third flush should succeed");
2825+
2826+
// Output should still be present
2827+
assert!(!test_output.stderr().is_empty());
2828+
}
2829+
2830+
#[test]
2831+
fn it_should_flush_empty_buffers_safely() {
2832+
let mut test_output = TestUserOutput::new(VerbosityLevel::Normal);
2833+
2834+
// Flushing with no output should be safe
2835+
test_output
2836+
.output
2837+
.flush()
2838+
.expect("Flushing empty buffers should succeed");
2839+
2840+
// No output should be present
2841+
assert_eq!(test_output.stderr(), "");
2842+
assert_eq!(test_output.stdout(), "");
2843+
}
2844+
2845+
#[test]
2846+
fn it_should_flush_both_stdout_and_stderr() {
2847+
let mut test_output = TestUserOutput::new(VerbosityLevel::Normal);
2848+
2849+
// Write to both channels
2850+
test_output.output.progress("Progress message");
2851+
test_output.output.result("Result data");
2852+
2853+
// Flush should handle both channels
2854+
test_output
2855+
.output
2856+
.flush()
2857+
.expect("Flush should succeed for both channels");
2858+
2859+
// Verify both outputs are present
2860+
assert!(test_output.stderr().contains("Progress message"));
2861+
assert!(test_output.stdout().contains("Result data"));
2862+
}
2863+
2864+
#[test]
2865+
fn it_should_work_with_sequential_flush_calls() {
2866+
let mut test_output = TestUserOutput::new(VerbosityLevel::Normal);
2867+
2868+
// Write, flush, write, flush pattern
2869+
test_output.output.progress("Message 1");
2870+
test_output
2871+
.output
2872+
.flush()
2873+
.expect("First flush should succeed");
2874+
2875+
test_output.output.progress("Message 2");
2876+
test_output
2877+
.output
2878+
.flush()
2879+
.expect("Second flush should succeed");
2880+
2881+
// Both messages should be present
2882+
let stderr = test_output.stderr();
2883+
assert!(stderr.contains("Message 1"));
2884+
assert!(stderr.contains("Message 2"));
2885+
}
2886+
}
27472887
}

0 commit comments

Comments
 (0)