|
| 1 | +//! Grouped reporter — buffers output per task, prints as a block on completion. |
| 2 | +
|
| 3 | +use std::{cell::RefCell, io::Write, process::ExitStatus as StdExitStatus, rc::Rc, sync::Arc}; |
| 4 | + |
| 5 | +use vite_path::AbsolutePath; |
| 6 | +use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind}; |
| 7 | + |
| 8 | +use super::{ |
| 9 | + ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter, |
| 10 | + StdioConfig, StdioSuggestion, format_command_with_cache_status, format_task_label, |
| 11 | + write_leaf_trailing_output, |
| 12 | +}; |
| 13 | +use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError}; |
| 14 | + |
| 15 | +mod writer; |
| 16 | + |
| 17 | +use writer::GroupedWriter; |
| 18 | + |
| 19 | +pub struct GroupedReporterBuilder { |
| 20 | + workspace_path: Arc<AbsolutePath>, |
| 21 | + writer: Box<dyn Write>, |
| 22 | +} |
| 23 | + |
| 24 | +impl GroupedReporterBuilder { |
| 25 | + pub fn new(workspace_path: Arc<AbsolutePath>, writer: Box<dyn Write>) -> Self { |
| 26 | + Self { workspace_path, writer } |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +impl GraphExecutionReporterBuilder for GroupedReporterBuilder { |
| 31 | + fn build(self: Box<Self>) -> Box<dyn GraphExecutionReporter> { |
| 32 | + Box::new(GroupedGraphReporter { |
| 33 | + writer: Rc::new(RefCell::new(self.writer)), |
| 34 | + workspace_path: self.workspace_path, |
| 35 | + }) |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +struct GroupedGraphReporter { |
| 40 | + writer: Rc<RefCell<Box<dyn Write>>>, |
| 41 | + workspace_path: Arc<AbsolutePath>, |
| 42 | +} |
| 43 | + |
| 44 | +impl GraphExecutionReporter for GroupedGraphReporter { |
| 45 | + fn new_leaf_execution( |
| 46 | + &mut self, |
| 47 | + display: &ExecutionItemDisplay, |
| 48 | + _leaf_kind: &LeafExecutionKind, |
| 49 | + ) -> Box<dyn LeafExecutionReporter> { |
| 50 | + let label = format_task_label(display); |
| 51 | + Box::new(GroupedLeafReporter { |
| 52 | + writer: Rc::clone(&self.writer), |
| 53 | + display: display.clone(), |
| 54 | + workspace_path: Arc::clone(&self.workspace_path), |
| 55 | + label, |
| 56 | + started: false, |
| 57 | + grouped_buffer: None, |
| 58 | + }) |
| 59 | + } |
| 60 | + |
| 61 | + fn finish(self: Box<Self>) -> Result<(), ExitStatus> { |
| 62 | + let mut writer = self.writer.borrow_mut(); |
| 63 | + let _ = writer.flush(); |
| 64 | + Ok(()) |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +struct GroupedLeafReporter { |
| 69 | + writer: Rc<RefCell<Box<dyn Write>>>, |
| 70 | + display: ExecutionItemDisplay, |
| 71 | + workspace_path: Arc<AbsolutePath>, |
| 72 | + label: vite_str::Str, |
| 73 | + started: bool, |
| 74 | + grouped_buffer: Option<Rc<RefCell<Vec<u8>>>>, |
| 75 | +} |
| 76 | + |
| 77 | +impl LeafExecutionReporter for GroupedLeafReporter { |
| 78 | + fn start(&mut self, cache_status: CacheStatus) -> StdioConfig { |
| 79 | + let line = |
| 80 | + format_command_with_cache_status(&self.display, &self.workspace_path, &cache_status); |
| 81 | + |
| 82 | + self.started = true; |
| 83 | + |
| 84 | + // Print labeled command line immediately (before output is buffered). |
| 85 | + let labeled_line = vite_str::format!("{} {line}", self.label); |
| 86 | + let mut writer = self.writer.borrow_mut(); |
| 87 | + let _ = writer.write_all(labeled_line.as_bytes()); |
| 88 | + let _ = writer.flush(); |
| 89 | + |
| 90 | + // Create shared buffer for both stdout and stderr. |
| 91 | + let buffer = Rc::new(RefCell::new(Vec::new())); |
| 92 | + self.grouped_buffer = Some(Rc::clone(&buffer)); |
| 93 | + |
| 94 | + StdioConfig { |
| 95 | + suggestion: StdioSuggestion::Piped, |
| 96 | + stdout_writer: Box::new(GroupedWriter::new(Rc::clone(&buffer))), |
| 97 | + stderr_writer: Box::new(GroupedWriter::new(buffer)), |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + fn finish( |
| 102 | + self: Box<Self>, |
| 103 | + _status: Option<StdExitStatus>, |
| 104 | + _cache_update_status: CacheUpdateStatus, |
| 105 | + error: Option<ExecutionError>, |
| 106 | + ) { |
| 107 | + // Build grouped block: header + buffered output. |
| 108 | + let mut extra = Vec::new(); |
| 109 | + if let Some(ref grouped_buffer) = self.grouped_buffer { |
| 110 | + let content = grouped_buffer.borrow(); |
| 111 | + if !content.is_empty() { |
| 112 | + let header = vite_str::format!("── {} ──\n", self.label); |
| 113 | + extra.extend_from_slice(header.as_bytes()); |
| 114 | + extra.extend_from_slice(&content); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + write_leaf_trailing_output(&self.writer, error, self.started, &extra); |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +#[cfg(test)] |
| 123 | +mod tests { |
| 124 | + use vite_task_plan::ExecutionItemKind; |
| 125 | + |
| 126 | + use super::*; |
| 127 | + use crate::session::{ |
| 128 | + event::CacheDisabledReason, |
| 129 | + reporter::{ |
| 130 | + StdioSuggestion, |
| 131 | + test_fixtures::{spawn_task, test_path}, |
| 132 | + }, |
| 133 | + }; |
| 134 | + |
| 135 | + fn leaf_kind(item: &vite_task_plan::ExecutionItem) -> &LeafExecutionKind { |
| 136 | + match &item.kind { |
| 137 | + ExecutionItemKind::Leaf(kind) => kind, |
| 138 | + ExecutionItemKind::Expanded(_) => panic!("test fixture item must be a Leaf"), |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + #[test] |
| 143 | + fn always_suggests_piped() { |
| 144 | + let task = spawn_task("build"); |
| 145 | + let item = &task.items[0]; |
| 146 | + |
| 147 | + let builder = Box::new(GroupedReporterBuilder::new(test_path(), Box::new(std::io::sink()))); |
| 148 | + let mut reporter = builder.build(); |
| 149 | + let mut leaf = reporter.new_leaf_execution(&item.execution_item_display, leaf_kind(item)); |
| 150 | + let stdio_config = leaf.start(CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata)); |
| 151 | + assert_eq!(stdio_config.suggestion, StdioSuggestion::Piped); |
| 152 | + } |
| 153 | +} |
0 commit comments