1use scope::market::series::{Metric, Sample};
38use std::collections::VecDeque;
39use std::fmt;
40use std::io::{self, Write};
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::sync::{Arc, Mutex};
43use std::time::Instant;
44
45type Sink = Arc<Mutex<dyn Write + Send>>;
47
48#[derive(Debug, Clone, Default)]
53pub struct RunProgress {
54 pub run: u64,
56 pub total: Option<u64>,
58 pub next_at: Option<Instant>,
60}
61
62pub type ProgressHook = Arc<Mutex<RunProgress>>;
64
65#[derive(Debug, Clone)]
67pub struct RunRecord {
68 pub run: u64,
70 pub at: chrono::DateTime<chrono::Utc>,
72 pub sample: Sample,
74 pub snapshot: String,
76}
77
78#[derive(Debug, Default)]
80pub struct Series {
81 pub metrics: Vec<Metric>,
83 pub runs: VecDeque<RunRecord>,
85 pub dropped: u64,
87}
88
89pub type SeriesHook = Arc<Mutex<Series>>;
91
92pub const MAX_SERIES_RUNS: usize = 500;
94
95#[derive(Clone)]
99pub struct Output {
100 out: Sink,
101 err: Sink,
102 progress: bool,
103 hook: Option<ProgressHook>,
104 series: Option<SeriesHook>,
105 tui: bool,
106}
107
108impl Output {
109 pub fn stdio() -> Self {
112 Self {
113 out: Arc::new(Mutex::new(io::stdout())),
114 err: Arc::new(Mutex::new(io::stderr())),
115 progress: true,
116 hook: None,
117 series: None,
118 tui: false,
119 }
120 }
121
122 pub fn capture() -> (Self, Captured) {
126 let out = Arc::new(Mutex::new(Vec::new()));
127 let err = Arc::new(Mutex::new(Vec::new()));
128 let captured = Captured {
129 data: out.clone(),
130 out: out.clone(),
131 err: err.clone(),
132 trimmed: Arc::default(),
133 };
134 let output = Self {
135 out,
136 err,
137 progress: false,
138 hook: None,
139 series: None,
140 tui: false,
141 };
142 (output, captured)
143 }
144
145 pub fn capture_merged() -> (Self, Captured) {
153 let buf = Arc::new(Mutex::new(Vec::new()));
154 let data = Arc::new(Mutex::new(Vec::new()));
155 let captured = Captured {
156 out: buf.clone(),
157 err: buf.clone(),
158 data: data.clone(),
159 trimmed: Arc::default(),
160 };
161 let output = Self {
162 out: Arc::new(Mutex::new(Tee {
163 merged: buf.clone(),
164 data,
165 })),
166 err: buf,
167 progress: false,
168 hook: Some(ProgressHook::default()),
169 series: Some(SeriesHook::default()),
170 tui: true,
171 };
172 (output, captured)
173 }
174
175 pub fn teed(&self, limit: usize) -> (Self, Captured) {
189 let buf = Arc::new(Mutex::new(Vec::new()));
190 let data = Arc::new(Mutex::new(Vec::new()));
191 let trimmed = Arc::new(AtomicBool::new(false));
192 let captured = Captured {
193 out: buf.clone(),
194 err: buf.clone(),
195 data: data.clone(),
196 trimmed: trimmed.clone(),
197 };
198 let copy = |real: &Sink, data: Option<Arc<Mutex<Vec<u8>>>>| -> Sink {
199 Arc::new(Mutex::new(TeeSink {
200 real: real.clone(),
201 merged: buf.clone(),
202 data,
203 limit,
204 trimmed: trimmed.clone(),
205 }))
206 };
207 let output = Self {
208 out: copy(&self.out, Some(data.clone())),
209 err: copy(&self.err, None),
210 progress: self.progress,
211 hook: None,
212 series: None,
213 tui: true,
214 };
215 (output, captured)
216 }
217
218 #[cfg(test)]
220 pub(crate) fn from_writers(
221 out: impl Write + Send + 'static,
222 err: impl Write + Send + 'static,
223 ) -> Self {
224 Self {
225 out: Arc::new(Mutex::new(out)),
226 err: Arc::new(Mutex::new(err)),
227 progress: false,
228 hook: None,
229 series: None,
230 tui: false,
231 }
232 }
233
234 pub fn progress_enabled(&self) -> bool {
236 self.progress
237 }
238
239 pub fn is_tui(&self) -> bool {
242 self.tui
243 }
244
245 pub fn progress_hook(&self) -> Option<ProgressHook> {
248 self.hook.clone()
249 }
250
251 pub fn series_hook(&self) -> Option<SeriesHook> {
254 self.series.clone()
255 }
256
257 pub fn record_run(&self, metrics: &[Metric], record: RunRecord) -> bool {
260 let Some(series) = &self.series else {
261 return false;
262 };
263 let mut series = lock(series);
264 if series.metrics.is_empty() {
265 series.metrics = metrics.to_vec();
266 }
267 series.runs.push_back(record);
268 while series.runs.len() > MAX_SERIES_RUNS {
269 series.runs.pop_front();
270 series.dropped += 1;
271 }
272 true
273 }
274
275 pub fn update_progress(&self, change: impl FnOnce(&mut RunProgress)) {
277 if let Some(hook) = &self.hook {
278 change(&mut lock(hook));
279 }
280 }
281
282 pub fn write_out(&self, args: fmt::Arguments<'_>) -> io::Result<()> {
289 emit(&self.out, args)
290 }
291
292 pub fn write_err(&self, args: fmt::Arguments<'_>) -> io::Result<()> {
298 emit(&self.err, args)
299 }
300
301 pub fn note(&self, args: fmt::Arguments<'_>) {
305 let _ = emit(&self.err, args);
306 }
307
308 pub fn note_saved(&self, what: &str, path: impl fmt::Display) {
310 crate::note!(self, "{what} saved to: {path}");
311 }
312}
313
314#[derive(Clone)]
316pub struct Captured {
317 data: Arc<Mutex<Vec<u8>>>,
319 out: Arc<Mutex<Vec<u8>>>,
320 err: Arc<Mutex<Vec<u8>>>,
321 trimmed: Arc<AtomicBool>,
323}
324
325impl Captured {
326 pub fn trimmed(&self) -> bool {
329 self.trimmed.load(Ordering::Relaxed)
330 }
331
332 pub fn out(&self) -> String {
334 String::from_utf8_lossy(&lock(&self.out)).into_owned()
335 }
336
337 pub fn data(&self) -> String {
340 String::from_utf8_lossy(&lock(&self.data)).into_owned()
341 }
342
343 pub fn err(&self) -> String {
345 String::from_utf8_lossy(&lock(&self.err)).into_owned()
346 }
347
348 pub fn out_sink(&self) -> scope::diag::Sink {
351 self.out.clone()
352 }
353}
354
355struct Tee {
358 merged: Arc<Mutex<Vec<u8>>>,
359 data: Arc<Mutex<Vec<u8>>>,
360}
361
362impl Write for Tee {
363 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
364 lock(&self.merged).extend_from_slice(buf);
365 lock(&self.data).extend_from_slice(buf);
366 Ok(buf.len())
367 }
368 fn flush(&mut self) -> io::Result<()> {
369 Ok(())
370 }
371}
372
373struct TeeSink {
376 real: Sink,
377 merged: Arc<Mutex<Vec<u8>>>,
378 data: Option<Arc<Mutex<Vec<u8>>>>,
380 limit: usize,
381 trimmed: Arc<AtomicBool>,
382}
383
384impl Write for TeeSink {
385 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
388 for copy in std::iter::once(&self.merged).chain(self.data.as_ref()) {
389 let mut copy = lock(copy);
390 copy.extend_from_slice(buf);
391 if keep_tail(&mut copy, self.limit) {
392 self.trimmed.store(true, Ordering::Relaxed);
393 }
394 }
395 lock(&self.real).write_all(buf)?;
396 Ok(buf.len())
397 }
398 fn flush(&mut self) -> io::Result<()> {
399 lock(&self.real).flush()
400 }
401}
402
403fn keep_tail(buf: &mut Vec<u8>, limit: usize) -> bool {
409 if buf.len() <= limit {
410 return false;
411 }
412 let cut = buf.len() - limit / 2;
413 let start = buf[cut..]
414 .iter()
415 .position(|&b| b == b'\n')
416 .map(|i| cut + i + 1)
417 .filter(|&start| start < buf.len())
418 .unwrap_or_else(|| whole_cut(buf, cut));
419 buf.drain(..start);
420 true
421}
422
423fn whole_cut(buf: &[u8], cut: usize) -> usize {
427 let mut i = 0;
428 while i < cut {
429 i = crate::cli::ansi::escape_end(buf, i).unwrap_or(i + 1);
430 }
431 let continuation = |b: u8| b & 0b1100_0000 == 0b1000_0000;
432 buf[i.min(buf.len())..]
433 .iter()
434 .position(|&b| !continuation(b))
435 .map_or(buf.len(), |n| i + n)
436}
437
438fn lock<T: ?Sized>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
441 m.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
442}
443
444fn emit(sink: &Mutex<dyn Write + Send>, args: fmt::Arguments<'_>) -> io::Result<()> {
449 lock(sink).write_fmt(args)
450}
451
452#[macro_export]
454macro_rules! outln {
455 ($o:expr) => {
456 $o.write_out(format_args!("\n"))
457 };
458 ($o:expr, $($arg:tt)*) => {
459 $o.write_out(format_args!("{}\n", format_args!($($arg)*)))
460 };
461}
462
463#[macro_export]
465macro_rules! out {
466 ($o:expr, $($arg:tt)*) => {
467 $o.write_out(format_args!($($arg)*))
468 };
469}
470
471#[macro_export]
473macro_rules! errln {
474 ($o:expr) => {
475 $o.write_err(format_args!("\n"))
476 };
477 ($o:expr, $($arg:tt)*) => {
478 $o.write_err(format_args!("{}\n", format_args!($($arg)*)))
479 };
480}
481
482#[macro_export]
485macro_rules! note {
486 ($o:expr) => {
487 $o.note(format_args!("\n"))
488 };
489 ($o:expr, $($arg:tt)*) => {
490 $o.note(format_args!("{}\n", format_args!($($arg)*)))
491 };
492}
493
494#[macro_export]
496macro_rules! err {
497 ($o:expr, $($arg:tt)*) => {
498 $o.write_err(format_args!($($arg)*))
499 };
500}
501
502#[cfg(test)]
507pub(crate) mod tests {
508 use super::*;
509
510 #[test]
511 fn test_capture_separates_data_from_diagnostics() {
512 let (o, cap) = Output::capture();
514 outln!(o, "{{\"ok\":{}}}", true).unwrap();
515 errln!(o, "Warning: {}", "partial data").unwrap();
516 assert_eq!(cap.out(), "{\"ok\":true}\n");
517 assert_eq!(cap.err(), "Warning: partial data\n");
518 }
519
520 #[test]
521 fn test_a_note_goes_to_the_diagnostic_channel_and_never_fails() {
522 let (o, cap) = Output::capture();
525 note!(o, "Fetching {}...", "USDC");
526 assert_eq!(cap.err(), "Fetching USDC...\n");
527 assert_eq!(cap.out(), "");
528 let closed = Output::from_writers(Vec::new(), ClosedPipe);
529 note!(closed, "dropped");
530 }
531
532 #[test]
533 fn test_note_saved_is_one_line_on_the_diagnostic_channel() {
534 let (o, cap) = Output::capture();
535 o.note_saved("Report", "r.md");
536 assert_eq!(cap.err(), "Report saved to: r.md\n");
537 assert_eq!(cap.out(), "");
538 }
539
540 #[test]
541 fn test_clones_share_writers() {
542 let (o, cap) = Output::capture();
544 let o2 = o.clone();
545 out!(o, "a").unwrap();
546 out!(o2, "b").unwrap();
547 outln!(o).unwrap();
548 assert_eq!(cap.out(), "ab\n");
549 }
550
551 pub(crate) struct ClosedPipe;
553
554 impl Write for ClosedPipe {
555 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
556 Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed"))
557 }
558 fn flush(&mut self) -> io::Result<()> {
559 Ok(())
560 }
561 }
562
563 #[test]
564 fn test_merged_capture_keeps_data_apart_from_diagnostics() {
565 let (o, cap) = Output::capture_merged();
566 outln!(o, "data").unwrap();
567 errln!(o, "warn").unwrap();
568 assert_eq!(cap.data(), "data\n");
569 assert_eq!(cap.out(), "data\nwarn\n");
570 }
571
572 #[test]
573 fn test_failed_write_is_returned_not_dropped() {
574 let o = Output::from_writers(ClosedPipe, ClosedPipe);
576 let e = outln!(o, "data").unwrap_err();
577 assert_eq!(e.kind(), io::ErrorKind::BrokenPipe);
578 assert_eq!(
579 errln!(o, "warn").unwrap_err().kind(),
580 io::ErrorKind::BrokenPipe
581 );
582 }
583
584 #[test]
585 fn test_only_the_tui_capture_is_tui() {
586 assert!(Output::capture_merged().0.is_tui());
587 assert!(!Output::capture().0.is_tui());
588 assert!(!Output::stdio().is_tui());
589 assert!(Output::stdio().teed(1).0.is_tui());
590 }
591
592 #[test]
593 fn test_capture_merged_keeps_write_order() {
594 let (o, cap) = Output::capture_merged();
597 outln!(o, "row 1").unwrap();
598 errln!(o, "warning").unwrap();
599 outln!(o, "row 2").unwrap();
600 assert_eq!(cap.out(), "row 1\nwarning\nrow 2\n");
601 }
602
603 #[test]
604 fn test_teed_output_reaches_the_terminal_and_keeps_a_copy() {
605 let (terminal, screen) = Output::capture();
608 let (o, copy) = terminal.teed(1024);
609 outln!(o, "row").unwrap();
610 errln!(o, "warn").unwrap();
611 assert_eq!(screen.out(), "row\n");
612 assert_eq!(screen.err(), "warn\n");
613 assert_eq!(copy.out(), "row\nwarn\n");
614 assert_eq!(copy.data(), "row\n");
615 assert!(!copy.trimmed());
616 assert!(o.is_tui());
619 assert_eq!(o.progress_enabled(), terminal.progress_enabled());
620 assert!(Output::stdio().teed(1).0.progress_enabled());
621 assert!(o.progress_hook().is_none());
622 }
623
624 #[test]
625 fn test_teed_copy_keeps_whole_lines_from_the_end_within_its_limit() {
626 let (terminal, screen) = Output::capture();
628 let (o, copy) = terminal.teed(100);
629 for i in 0..1000 {
630 outln!(o, "line {i:04}").unwrap();
631 }
632 assert!(copy.trimmed());
633 let kept = copy.out();
634 assert!(kept.len() <= 100, "{}", kept.len());
635 assert!(kept.starts_with("line "), "whole lines: {kept:?}");
636 assert!(kept.ends_with("line 0999\n"), "{kept:?}");
637 assert!(copy.data().ends_with("line 0999\n"));
638 assert_eq!(screen.out().lines().count(), 1000, "the terminal got all");
639 }
640
641 #[test]
642 fn test_teed_copy_keeps_the_tail_of_one_long_line() {
643 let (terminal, _screen) = Output::capture();
646 let (o, copy) = terminal.teed(100);
647 let blob: String = (0..300)
648 .map(|i| char::from(b'a' + (i % 26) as u8))
649 .collect();
650 out!(o, "{blob}").unwrap();
651 assert!(copy.trimmed());
652 let kept = copy.out();
653 assert!(!kept.is_empty() && kept.len() <= 100, "{}", kept.len());
654 assert!(blob.ends_with(&kept), "the end of the line: {kept:?}");
655 }
656
657 #[test]
658 fn test_trim_of_one_line_splits_no_character_and_no_escape() {
659 let mut chars = "é".repeat(60).into_bytes(); assert!(keep_tail(&mut chars, 102)); assert!(String::from_utf8(chars.clone()).is_ok(), "{chars:?}");
664 assert_eq!(chars.len(), 50);
665 let mut colored = b"x".repeat(60);
666 colored.extend_from_slice(b"\x1b[38;5;196mred");
667 assert!(keep_tail(&mut colored, 20));
669 assert_eq!(colored, b"red");
670 let mut link = b"y".repeat(60);
671 link.extend_from_slice(b"\x1b]8;;https://example.com/a/long/path\x1b\\text");
672 let cut_at = link.len() - 40 / 2;
674 assert!(cut_at > 60 && cut_at < 98, "{cut_at}");
675 assert!(keep_tail(&mut link, 40));
676 assert_eq!(link, b"text");
677 }
678
679 #[test]
680 fn test_teed_copy_keeps_text_the_terminal_refused() {
681 let (o, copy) = Output::from_writers(ClosedPipe, ClosedPipe).teed(64);
683 assert!(outln!(o, "data").is_err());
684 assert_eq!(copy.out(), "data\n");
685 }
686
687 #[test]
688 fn test_capture_disables_progress() {
689 let (o, _) = Output::capture();
691 assert!(!o.progress_enabled());
692 assert!(Output::stdio().progress_enabled());
693 }
694}