1use std::io::{self, Write};
267use std::marker::PhantomData;
268use std::sync::Arc;
269use std::sync::atomic::{AtomicBool, Ordering};
270use std::time::{Duration, Instant};
271
272use owo_colors::{OwoColorize, Style};
273use tracing::field::Visit;
274use tracing::{Event, Id};
275use tracing_subscriber::fmt::MakeWriter;
276use tracing_subscriber::layer::{Context, Layer};
277use tracing_subscriber::registry::LookupSpan;
278
279#[derive(Clone, Debug)]
302pub struct ColorScheme {
303 pub success: Style,
305 pub error: Style,
307 pub warning: Style,
309 pub notice: Style,
311 pub group: Style,
313 pub tree: Style,
315 pub extra: Style,
317 pub succeeded: Style,
319 pub failed: Style,
321 pub duration: Style,
323}
324
325impl Default for ColorScheme {
326 fn default() -> Self {
328 Self {
329 success: Style::new().green(),
330 error: Style::new().red(),
331 warning: Style::new().yellow(),
332 notice: Style::new().purple(),
333 group: Style::new().dimmed(),
334 tree: Style::new().blue(),
335 extra: Style::new().cyan(),
336 succeeded: Style::new().green().bold(),
337 failed: Style::new().red().bold(),
338 duration: Style::new().dimmed(),
339 }
340 }
341}
342
343impl ColorScheme {
344 pub fn new() -> Self {
346 Self::default()
347 }
348
349 pub fn with_success_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
360 self.success = Style::new().fg::<C>();
361 self
362 }
363
364 pub fn with_success_style(mut self, style: Style) -> Self {
366 self.success = style;
367 self
368 }
369
370 pub fn with_error_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
372 self.error = Style::new().fg::<C>();
373 self
374 }
375
376 pub fn with_error_style(mut self, style: Style) -> Self {
378 self.error = style;
379 self
380 }
381
382 pub fn with_warning_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
384 self.warning = Style::new().fg::<C>();
385 self
386 }
387
388 pub fn with_warning_style(mut self, style: Style) -> Self {
390 self.warning = style;
391 self
392 }
393
394 pub fn with_notice_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
396 self.notice = Style::new().fg::<C>();
397 self
398 }
399
400 pub fn with_notice_style(mut self, style: Style) -> Self {
402 self.notice = style;
403 self
404 }
405
406 pub fn with_group_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
408 self.group = Style::new().fg::<C>();
409 self
410 }
411
412 pub fn with_group_style(mut self, style: Style) -> Self {
414 self.group = style;
415 self
416 }
417
418 pub fn with_tree_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
420 self.tree = Style::new().fg::<C>();
421 self
422 }
423
424 pub fn with_tree_style(mut self, style: Style) -> Self {
426 self.tree = style;
427 self
428 }
429
430 pub fn with_extra_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
432 self.extra = Style::new().fg::<C>();
433 self
434 }
435
436 pub fn with_extra_style(mut self, style: Style) -> Self {
438 self.extra = style;
439 self
440 }
441
442 pub fn with_succeeded_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
444 self.succeeded = Style::new().fg::<C>().bold();
445 self
446 }
447
448 pub fn with_succeeded_style(mut self, style: Style) -> Self {
450 self.succeeded = style;
451 self
452 }
453
454 pub fn with_failed_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
456 self.failed = Style::new().fg::<C>().bold();
457 self
458 }
459
460 pub fn with_failed_style(mut self, style: Style) -> Self {
462 self.failed = style;
463 self
464 }
465
466 pub fn with_duration_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
468 self.duration = Style::new().fg::<C>();
469 self
470 }
471
472 pub fn with_duration_style(mut self, style: Style) -> Self {
474 self.duration = style;
475 self
476 }
477}
478
479pub trait Status: Send + Sync {
484 fn from_str(s: &str) -> Self;
489
490 fn write_icon<W: Write>(&self, out: W, colors: &ColorScheme) -> io::Result<()>;
492
493 fn is_passthrough(&self) -> bool;
496}
497
498pub trait StatusFactory<S: Status>: Send + Sync {
503 fn group(&self) -> S;
505
506 fn success(&self) -> S;
508
509 fn failure(&self) -> S;
511}
512
513#[derive(Debug, Clone, Copy)]
515pub enum DefaultStatus {
516 Notice,
517 Success,
518 Failure,
519 Warning,
520 Group,
521 Pass,
522}
523
524impl Status for DefaultStatus {
525 fn from_str(s: &str) -> Self {
526 match s {
527 "notice" => Self::Notice,
528 "success" => Self::Success,
529 "warn" => Self::Warning,
530 "error" => Self::Failure,
531 "pass" => Self::Pass,
532 _ => Self::Success,
533 }
534 }
535
536 fn write_icon<W: Write>(&self, mut out: W, colors: &ColorScheme) -> io::Result<()> {
537 match self {
538 Self::Notice => write!(out, "{}", colors.notice.style("!")),
539 Self::Success => write!(out, "{}", colors.success.style("✔")),
540 Self::Failure => write!(out, "{}", colors.error.style("⨯")),
541 Self::Warning => write!(out, "{}", colors.warning.style("⚠")),
542 Self::Group => write!(out, "{}", colors.group.style("⚙")),
543 Self::Pass => write!(out, ""),
544 }
545 }
546
547 fn is_passthrough(&self) -> bool {
548 matches!(self, Self::Pass)
549 }
550}
551
552#[derive(Debug, Clone, Copy, Default)]
554pub struct DefaultStatusFactory;
555
556impl StatusFactory<DefaultStatus> for DefaultStatusFactory {
557 fn group(&self) -> DefaultStatus {
558 DefaultStatus::Group
559 }
560
561 fn success(&self) -> DefaultStatus {
562 DefaultStatus::Success
563 }
564
565 fn failure(&self) -> DefaultStatus {
566 DefaultStatus::Failure
567 }
568}
569
570fn format_duration(d: Duration) -> String {
577 let nanos = d.as_nanos();
578 if nanos >= 999_950_000 {
580 format!("{:.1}s", d.as_secs_f64())
581 } else if nanos >= 999_950 {
582 format!("{:.1}ms", nanos as f64 / 1_000_000.0)
584 } else if nanos >= 1_000 {
585 format!("{:.1}µs", nanos as f64 / 1_000.0)
586 } else {
587 format!("{nanos}ns")
588 }
589}
590
591#[derive(Debug)]
597pub struct ConsoleLayer<W, F = DefaultStatusFactory, S = DefaultStatus> {
598 make_writer: W,
599 status_factory: F,
600 colors: Arc<ColorScheme>,
601 _status: PhantomData<S>,
602}
603
604impl Default for ConsoleLayer<fn() -> io::Stderr> {
605 fn default() -> Self {
610 Self {
611 make_writer: io::stderr,
612 status_factory: DefaultStatusFactory,
613 colors: Arc::new(ColorScheme::default()),
614 _status: PhantomData,
615 }
616 }
617}
618
619impl<W> ConsoleLayer<W> {
620 pub fn with_writer(make_writer: W) -> Self {
625 Self {
626 make_writer,
627 status_factory: DefaultStatusFactory,
628 colors: Arc::new(ColorScheme::default()),
629 _status: PhantomData,
630 }
631 }
632}
633
634impl<W, F, S> ConsoleLayer<W, F, S> {
635 const HEADER_START: &str = "╭─";
640 const FOOTER_START: &str = "╰─";
642 const GROUP_START: &str = "╭─";
644 const GROUP_END: &str = "╰─";
646 const ITEM_START: &str = "├─";
648 const PIPE: &str = "│ ";
650 const PIPE_PREFIX: &str = "│ ";
652 const CLOCK: &str = "⏱";
654
655 pub fn with_status<CustomF, CustomS>(
657 self,
658 status_factory: CustomF,
659 ) -> ConsoleLayer<W, CustomF, CustomS>
660 where
661 CustomF: StatusFactory<CustomS>,
662 CustomS: Status,
663 {
664 ConsoleLayer {
665 make_writer: self.make_writer,
666 status_factory,
667 colors: self.colors,
668 _status: PhantomData,
669 }
670 }
671
672 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
690 self.colors = Arc::new(colors);
691 self
692 }
693
694 fn print_line<St: Status>(
700 &self,
701 depth: usize,
702 start_symbol: &str,
703 status: St,
704 message: &str,
705 duration: Option<Duration>,
706 bold: bool,
707 ) where
708 W: for<'writer> MakeWriter<'writer>,
709 {
710 let mut out = self.make_writer.make_writer();
711
712 if depth > 0 {
713 write!(out, "{}", self.colors.tree.style(Self::PIPE)).ok();
715 for _ in 1..depth {
716 write!(out, "{}", self.colors.tree.style(Self::PIPE_PREFIX)).ok();
717 }
718 }
719
720 write!(out, "{}", self.colors.tree.style(start_symbol)).ok();
721 write!(out, " ").ok();
722 status.write_icon(&mut out, &self.colors).ok();
723 if bold {
724 write!(out, " {}", message.bold()).ok();
725 } else {
726 write!(out, " {}", message).ok();
727 }
728 if let Some(d) = duration {
729 write!(
730 out,
731 " {}",
732 self.colors
733 .duration
734 .style(format!("{} {} ", Self::CLOCK, format_duration(d)))
735 )
736 .ok();
737 }
738 writeln!(out).ok();
739 }
740}
741
742#[derive(Debug)]
743struct ConsoleSpanInfo {
744 extra: Option<String>,
745 has_failed: AtomicBool,
746}
747
748impl<S, W, F, St> Layer<S> for ConsoleLayer<W, F, St>
749where
750 S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
751 W: for<'writer> MakeWriter<'writer> + 'static + Send + Sync,
752 F: StatusFactory<St> + 'static,
753 St: Status + 'static,
754{
755 fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
762 if let Some(span) = ctx.span(id) {
763 let mut visitor = FieldVisitor::default();
764 attrs.record(&mut visitor);
765
766 span.extensions_mut().insert(ConsoleSpanInfo {
767 extra: visitor.extra,
768 has_failed: AtomicBool::new(false),
769 });
770 }
771 }
772
773 fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
780 if let Some(span) = ctx.span(id) {
781 if span.extensions().get::<Instant>().is_some() {
782 return;
783 }
784 span.extensions_mut().insert(Instant::now());
785
786 let metadata = span.metadata();
787 let extensions = span.extensions();
788 let info = extensions.get::<ConsoleSpanInfo>().unwrap();
789 let depth = span.scope().from_root().count() - 1;
790 let start_symbol = if depth == 0 {
791 Self::HEADER_START
792 } else {
793 Self::GROUP_START
794 };
795 let message = if let Some(extra) = &info.extra {
796 format!("{} {}", metadata.name(), self.colors.extra.style(extra))
797 } else {
798 metadata.name().to_string()
799 };
800 self.print_line(
801 depth,
802 start_symbol,
803 self.status_factory.group(),
804 &message,
805 None,
806 true,
807 );
808 }
809 }
810
811 fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
825 let mut visitor = FieldVisitor::default();
826 event.record(&mut visitor);
827
828 let is_failure = *event.metadata().level() == tracing::Level::ERROR;
831
832 let mut message = visitor.message.clone();
833
834 let was_already_failed = if is_failure && !visitor.console {
839 ctx.event_scope(event)
840 .and_then(|mut scope| scope.next())
841 .and_then(|span| {
842 span.extensions()
843 .get::<ConsoleSpanInfo>()
844 .map(|info| info.has_failed.load(Ordering::SeqCst))
845 })
846 .unwrap_or(false)
847 } else {
848 false
849 };
850
851 if is_failure {
852 if let Some(scope) = ctx.event_scope(event) {
854 for span in scope.from_root() {
855 if let Some(info) = span.extensions().get::<ConsoleSpanInfo>() {
856 info.has_failed.store(true, Ordering::SeqCst);
857 }
858 }
859 }
860 if let Some(error) = visitor.error {
862 if message.is_empty() {
863 message = error;
864 } else {
865 message = format!("{message}: {error}");
866 }
867 }
868 }
869
870 if visitor.console || (is_failure && !was_already_failed) {
874 let status = if visitor.console {
875 St::from_str(&visitor.status)
876 } else {
877 self.status_factory.failure()
879 };
880
881 let (start_symbol, bold) = if status.is_passthrough() {
882 ("", false)
883 } else {
884 (Self::ITEM_START, true)
885 };
886
887 let depth = ctx.event_scope(event).map_or(0, |scope| scope.count());
888 self.print_line(depth, start_symbol, status, &message, None, bold);
889 }
890 }
891
892 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
902 if let Some(span) = ctx.span(&id) {
903 let extensions = span.extensions();
904 let info = extensions.get::<ConsoleSpanInfo>().unwrap();
905
906 let duration = extensions
907 .get::<Instant>()
908 .map(|start_time| start_time.elapsed());
909
910 let metadata = span.metadata();
911 let has_failed = info.has_failed.load(Ordering::SeqCst);
912 let status = if has_failed {
913 self.status_factory.failure()
914 } else {
915 self.status_factory.success()
916 };
917
918 let base_message = if let Some(extra) = &info.extra {
919 format!("{} {}", metadata.name(), self.colors.extra.style(extra))
920 } else {
921 metadata.name().to_string()
922 };
923
924 let depth = span.scope().from_root().count() - 1;
925 let (final_message, start_symbol) = if depth == 0 {
926 let status_string = if has_failed {
927 self.colors.failed.style("failed").to_string()
928 } else {
929 self.colors.succeeded.style("succeeded").to_string()
930 };
931 (
932 format!("{base_message} {status_string}"),
933 Self::FOOTER_START,
934 )
935 } else {
936 (base_message, Self::GROUP_END)
937 };
938 self.print_line(depth, start_symbol, status, &final_message, duration, true);
939 }
940 }
941}
942
943#[derive(Default)]
951struct FieldVisitor {
952 status: String,
953 message: String,
954 console: bool,
955 extra: Option<String>,
956 error: Option<String>,
957}
958
959impl Visit for FieldVisitor {
960 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
962 match field.name() {
963 "message" => self.message = value.to_string(),
964 "status" => self.status = value.to_string(),
965 "extra" => self.extra = Some(value.to_string()),
966 _ => {}
967 }
968 }
969
970 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
973 if let "console" = field.name() {
974 self.console = value
975 }
976 }
977
978 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
981 if field.name() == "error" {
982 self.error = Some(format!("{value:?}"));
983 }
984 }
985}
986
987#[doc(hidden)]
988pub mod macros {
1000 use owo_colors::OwoColorize;
1001 use std::fmt::Display;
1002
1003 pub struct Displayable<T>(pub T);
1010
1011 pub trait ConsoleResult<T> {
1021 fn format_success(self, colorize: impl Fn(String) -> String, message: String) -> String;
1023 }
1024
1025 impl ConsoleResult<()> for () {
1026 fn format_success(self, _colorize: impl Fn(String) -> String, message: String) -> String {
1027 message
1028 }
1029 }
1030
1031 impl<T: Display> ConsoleResult<Displayable<T>> for T {
1032 fn format_success(self, colorize: impl Fn(String) -> String, message: String) -> String {
1033 let this = self.to_string();
1034 if this.is_empty() {
1035 message
1036 } else {
1037 format!("{}: {}", message, colorize(self.to_string()))
1038 }
1039 }
1040 }
1041
1042 pub fn format_failure<E: Display>(err: &E, message: String) -> String {
1048 format!("{}: {}", message, err.to_string().red())
1049 }
1050}
1051
1052#[doc(hidden)]
1053#[macro_export]
1056macro_rules! __console_impl_result {
1057 ($result:expr, $colorize:expr, $($arg:tt)+) => {
1058 match $result {
1059 Ok(result) => {
1060 use $crate::macros::ConsoleResult;
1061 let message = result.format_success($colorize, format!($($arg)+));
1062 tracing::event!(
1063 tracing::Level::INFO,
1064 console = true,
1065 status = "success",
1066 message = message
1067 );
1068 Ok(result)
1069 }
1070 Err(e) => {
1071 let error_message = $crate::macros::format_failure(&e, format!($($arg)+));
1072 tracing::event!(
1073 tracing::Level::ERROR,
1074 console = true,
1075 status = "error",
1076 message = error_message
1077 );
1078 Err(e)
1079 }
1080 }
1081 };
1082}
1083
1084#[macro_export]
1160macro_rules! console {
1161 (pass, $($arg:tt)+) => {
1162 tracing::event!(tracing::Level::INFO, console = true, status = "pass", message = &format!($($arg)+));
1163 };
1164 (notice, $($arg:tt)+) => {
1165 tracing::event!(tracing::Level::INFO, console = true, status = "notice", message = &format!($($arg)+));
1166 };
1167 (info, $($arg:tt)+) => {
1168 tracing::event!(tracing::Level::INFO, console = true, status = "success", message = &format!($($arg)+));
1169 };
1170 (warn, $($arg:tt)+) => {
1171 tracing::event!(tracing::Level::WARN, console = true, status = "warn", message = &format!($($arg)+));
1172 };
1173 (error, $($arg:tt)+) => {
1174 tracing::event!(tracing::Level::ERROR, console = true, status = "error", message = &format!($($arg)+));
1175 };
1176 ($result:expr, $color:expr, $($arg:tt)+) => {
1177 $crate::__console_impl_result!($result, $color, $($arg)+)
1178 };
1179 ($result:expr, $($arg:tt)+) => {
1180 $crate::__console_impl_result!($result, |s: String| s.green().to_string(), $($arg)+)
1181 };
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use super::*;
1187 use regex::Regex;
1188 use std::fmt::Display;
1189 use std::sync::{Arc, Mutex};
1190 use tracing_subscriber::Registry;
1191 use tracing_subscriber::fmt::MakeWriter;
1192 use tracing_subscriber::prelude::*;
1193
1194 #[derive(Debug, Clone)]
1195 struct MockWriter {
1196 buf: Arc<Mutex<Vec<u8>>>,
1197 }
1198
1199 impl Display for MockWriter {
1200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1201 write!(
1202 f,
1203 "{}",
1204 String::from_utf8(self.buf.lock().unwrap().clone()).unwrap()
1205 )
1206 }
1207 }
1208
1209 impl MockWriter {
1210 fn new() -> Self {
1211 Self {
1212 buf: Arc::new(Mutex::new(Vec::new())),
1213 }
1214 }
1215 }
1216
1217 impl<'a> MakeWriter<'a> for MockWriter {
1218 type Writer = Self;
1219
1220 fn make_writer(&'a self) -> Self::Writer {
1221 self.clone()
1222 }
1223 }
1224
1225 impl io::Write for MockWriter {
1226 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1227 self.buf.lock().unwrap().write(buf)
1228 }
1229
1230 fn flush(&mut self) -> io::Result<()> {
1231 self.buf.lock().unwrap().flush()
1232 }
1233 }
1234
1235 fn strip_non_deterministic(output: &str) -> String {
1236 let output = strip_ansi_escapes::strip_str(output);
1238
1239 let duration_regex = Regex::new(r" ⏱ .*? ").unwrap();
1241 duration_regex.replace_all(&output, "").trim().to_string()
1242 }
1243
1244 #[test]
1245 fn test_simple_span() {
1246 let writer = MockWriter::new();
1247 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1248
1249 tracing::subscriber::with_default(subscriber, || {
1250 let _span = tracing::info_span!("simple").entered();
1251 console!(info, "It works!");
1252 });
1253
1254 let output = strip_non_deterministic(&writer.to_string());
1255 insta::assert_snapshot!(output, @r###"
1256 ╭─ ⚙ simple
1257 │ ├─ ✔ It works!
1258 ╰─ ✔ simple succeeded
1259 "###);
1260 }
1261
1262 #[test]
1263 fn test_nested_spans() {
1264 let writer = MockWriter::new();
1265 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1266
1267 tracing::subscriber::with_default(subscriber, || {
1268 let _outer = tracing::info_span!("outer").entered();
1269 let _inner = tracing::info_span!("inner").entered();
1270 console!(notice, "Something is happening");
1271 });
1272
1273 let output = strip_non_deterministic(&writer.to_string());
1274 insta::assert_snapshot!(output, @r###"
1275 ╭─ ⚙ outer
1276 │ ╭─ ⚙ inner
1277 │ │ ├─ ! Something is happening
1278 │ ╰─ ✔ inner
1279 ╰─ ✔ outer succeeded
1280 "###);
1281 }
1282
1283 #[test]
1284 fn test_error_propagation() {
1285 let writer = MockWriter::new();
1286 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1287
1288 tracing::subscriber::with_default(subscriber, || {
1289 #[tracing::instrument(name = "inner", err)]
1290 fn inner() -> Result<(), &'static str> {
1291 Err("An error occurred")
1292 }
1293 let _outer = tracing::info_span!("outer").entered();
1294 inner().ok()
1295 });
1296
1297 let output = strip_non_deterministic(&writer.to_string());
1298 insta::assert_snapshot!(output, @r"
1299 ╭─ ⚙ outer
1300 │ ╭─ ⚙ inner
1301 │ │ ├─ ⨯ An error occurred
1302 │ ╰─ ⨯ inner
1303 ╰─ ⨯ outer failed
1304 ");
1305 }
1306
1307 #[test]
1308 fn test_nested_instrument_err_deduplication() {
1309 let writer = MockWriter::new();
1310 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1311
1312 tracing::subscriber::with_default(subscriber, || {
1313 #[tracing::instrument(name = "inner", err)]
1314 fn inner() -> Result<(), &'static str> {
1315 Err("something went wrong")
1316 }
1317
1318 #[tracing::instrument(name = "middle", err)]
1319 fn middle() -> Result<(), &'static str> {
1320 inner()
1321 }
1322
1323 #[tracing::instrument(name = "outer", err)]
1324 fn outer() -> Result<(), &'static str> {
1325 middle()
1326 }
1327
1328 let _root = tracing::info_span!("root").entered();
1329 outer().ok();
1330 });
1331
1332 let output = strip_non_deterministic(&writer.to_string());
1333 insta::assert_snapshot!(output, @r"
1336 ╭─ ⚙ root
1337 │ ╭─ ⚙ outer
1338 │ │ ╭─ ⚙ middle
1339 │ │ │ ╭─ ⚙ inner
1340 │ │ │ │ ├─ ⨯ something went wrong
1341 │ │ │ ╰─ ⨯ inner
1342 │ │ ╰─ ⨯ middle
1343 │ ╰─ ⨯ outer
1344 ╰─ ⨯ root failed
1345 ");
1346 }
1347
1348 #[test]
1353 fn test_bump_branch_already_exists() {
1354 let writer = MockWriter::new();
1355 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1356
1357 tracing::subscriber::with_default(subscriber, || {
1358 #[tracing::instrument(name = "run command git checkout -b bump/v0.15.4", err)]
1359 fn git_checkout_new_branch() -> Result<(), String> {
1360 console!(warn, "fatal: a branch named 'bump/v0.15.4' already exists");
1361 Err(
1362 "error running command git checkout -b bump/v0.15.4, exit code: Some(128)"
1363 .to_string(),
1364 )
1365 }
1366
1367 #[tracing::instrument(name = "bump", err)]
1368 fn bump_inner() -> Result<(), String> {
1369 {
1370 let _span = tracing::info_span!("configure git").entered();
1371 {
1372 let _cmd =
1373 tracing::info_span!("run command git config user.name").entered();
1374 }
1375 {
1376 let _cmd =
1377 tracing::info_span!("run command git config user.email").entered();
1378 }
1379 }
1380 {
1381 let _span = tracing::info_span!("fetch and checkout").entered();
1382 {
1383 let _cmd =
1384 tracing::info_span!("run command git fetch origin develop").entered();
1385 }
1386 {
1387 let _cmd = tracing::info_span!("run command git checkout origin/develop")
1388 .entered();
1389 }
1390 }
1391 console!(info, "current version: 0.15.3");
1392 console!(info, "new version: 0.15.4");
1393 {
1394 let _cmd = tracing::info_span!(
1395 "run command git ls-remote --exit-code --heads origin bump/v0.15.4"
1396 )
1397 .entered();
1398 }
1399 git_checkout_new_branch()
1400 }
1401
1402 #[tracing::instrument(name = "bump", err)]
1403 fn bump_outer() -> Result<(), String> {
1404 bump_inner()
1405 }
1406
1407 let _cli = tracing::info_span!("cli").entered();
1408 console!(notice, "run id: 019c7593-1f68-784a-ade1-9a06a33a38be");
1409 bump_outer().ok();
1410 console!(notice, "run id: 019c7593-1f68-784a-ade1-9a06a33a38be");
1411 });
1412
1413 let output = strip_non_deterministic(&writer.to_string());
1414 insta::assert_snapshot!(output, @r"
1415 ╭─ ⚙ cli
1416 │ ├─ ! run id: 019c7593-1f68-784a-ade1-9a06a33a38be
1417 │ ╭─ ⚙ bump
1418 │ │ ╭─ ⚙ bump
1419 │ │ │ ╭─ ⚙ configure git
1420 │ │ │ │ ╭─ ⚙ run command git config user.name
1421 │ │ │ │ ╰─ ✔ run command git config user.name
1422 │ │ │ │ ╭─ ⚙ run command git config user.email
1423 │ │ │ │ ╰─ ✔ run command git config user.email
1424 │ │ │ ╰─ ✔ configure git
1425 │ │ │ ╭─ ⚙ fetch and checkout
1426 │ │ │ │ ╭─ ⚙ run command git fetch origin develop
1427 │ │ │ │ ╰─ ✔ run command git fetch origin develop
1428 │ │ │ │ ╭─ ⚙ run command git checkout origin/develop
1429 │ │ │ │ ╰─ ✔ run command git checkout origin/develop
1430 │ │ │ ╰─ ✔ fetch and checkout
1431 │ │ │ ├─ ✔ current version: 0.15.3
1432 │ │ │ ├─ ✔ new version: 0.15.4
1433 │ │ │ ╭─ ⚙ run command git ls-remote --exit-code --heads origin bump/v0.15.4
1434 │ │ │ ╰─ ✔ run command git ls-remote --exit-code --heads origin bump/v0.15.4
1435 │ │ │ ╭─ ⚙ run command git checkout -b bump/v0.15.4
1436 │ │ │ │ ├─ ⚠ fatal: a branch named 'bump/v0.15.4' already exists
1437 │ │ │ │ ├─ ⨯ error running command git checkout -b bump/v0.15.4, exit code: Some(128)
1438 │ │ │ ╰─ ⨯ run command git checkout -b bump/v0.15.4
1439 │ │ ╰─ ⨯ bump
1440 │ ╰─ ⨯ bump
1441 │ ├─ ! run id: 019c7593-1f68-784a-ade1-9a06a33a38be
1442 ╰─ ⨯ cli failed
1443 ");
1444 }
1445
1446 #[test]
1451 fn test_bump_push_permission_denied() {
1452 let writer = MockWriter::new();
1453 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1454
1455 tracing::subscriber::with_default(subscriber, || {
1456 #[tracing::instrument(name = "run command git push -u origin HEAD", err)]
1457 fn git_push() -> Result<(), String> {
1458 console!(
1459 warn,
1460 "remote: You are not allowed to push code to this project."
1461 );
1462 console!(
1463 warn,
1464 "fatal: unable to access 'https://git.example.com/example/project.git/': The requested URL returned error: 403"
1465 );
1466 Err("You are not allowed to push code to this project.".to_string())
1467 }
1468
1469 #[tracing::instrument(name = "commit and push", err)]
1470 fn commit_and_push() -> Result<(), String> {
1471 {
1472 let _cmd =
1473 tracing::info_span!("run command git add Cargo.toml Cargo.lock").entered();
1474 }
1475 {
1476 let _cmd =
1477 tracing::info_span!("run command git commit -m bump version to 0.15.4")
1478 .entered();
1479 }
1480 git_push()
1481 }
1482
1483 #[tracing::instrument(name = "bump", err)]
1484 fn bump_inner() -> Result<(), String> {
1485 {
1486 let _span = tracing::info_span!("configure git").entered();
1487 }
1488 {
1489 let _span = tracing::info_span!("fetch and checkout").entered();
1490 }
1491 console!(info, "current version: 0.15.3");
1492 console!(info, "new version: 0.15.4");
1493 {
1494 let _cmd =
1495 tracing::info_span!("run command git checkout -b bump/v0.15.4").entered();
1496 }
1497 {
1498 let _span = tracing::info_span!("update cargo.toml").entered();
1499 console!(info, "updated Cargo.toml to version 0.15.4");
1500 }
1501 {
1502 let _span = tracing::info_span!("update cargo.lock").entered();
1503 {
1504 let _cmd =
1505 tracing::info_span!("run command cargo update --workspace").entered();
1506 }
1507 console!(info, "updated Cargo.lock");
1508 }
1509 commit_and_push()
1510 }
1511
1512 #[tracing::instrument(name = "bump", err)]
1513 fn bump_outer() -> Result<(), String> {
1514 bump_inner()
1515 }
1516
1517 let _cli = tracing::info_span!("cli").entered();
1518 console!(notice, "run id: 019c75a8-e7a6-7bec-822a-e371c363d73e");
1519 bump_outer().ok();
1520 console!(notice, "run id: 019c75a8-e7a6-7bec-822a-e371c363d73e");
1521 });
1522
1523 let output = strip_non_deterministic(&writer.to_string());
1524 insta::assert_snapshot!(output, @r"
1525 ╭─ ⚙ cli
1526 │ ├─ ! run id: 019c75a8-e7a6-7bec-822a-e371c363d73e
1527 │ ╭─ ⚙ bump
1528 │ │ ╭─ ⚙ bump
1529 │ │ │ ╭─ ⚙ configure git
1530 │ │ │ ╰─ ✔ configure git
1531 │ │ │ ╭─ ⚙ fetch and checkout
1532 │ │ │ ╰─ ✔ fetch and checkout
1533 │ │ │ ├─ ✔ current version: 0.15.3
1534 │ │ │ ├─ ✔ new version: 0.15.4
1535 │ │ │ ╭─ ⚙ run command git checkout -b bump/v0.15.4
1536 │ │ │ ╰─ ✔ run command git checkout -b bump/v0.15.4
1537 │ │ │ ╭─ ⚙ update cargo.toml
1538 │ │ │ │ ├─ ✔ updated Cargo.toml to version 0.15.4
1539 │ │ │ ╰─ ✔ update cargo.toml
1540 │ │ │ ╭─ ⚙ update cargo.lock
1541 │ │ │ │ ╭─ ⚙ run command cargo update --workspace
1542 │ │ │ │ ╰─ ✔ run command cargo update --workspace
1543 │ │ │ │ ├─ ✔ updated Cargo.lock
1544 │ │ │ ╰─ ✔ update cargo.lock
1545 │ │ │ ╭─ ⚙ commit and push
1546 │ │ │ │ ╭─ ⚙ run command git add Cargo.toml Cargo.lock
1547 │ │ │ │ ╰─ ✔ run command git add Cargo.toml Cargo.lock
1548 │ │ │ │ ╭─ ⚙ run command git commit -m bump version to 0.15.4
1549 │ │ │ │ ╰─ ✔ run command git commit -m bump version to 0.15.4
1550 │ │ │ │ ╭─ ⚙ run command git push -u origin HEAD
1551 │ │ │ │ │ ├─ ⚠ remote: You are not allowed to push code to this project.
1552 │ │ │ │ │ ├─ ⚠ fatal: unable to access 'https://git.example.com/example/project.git/': The requested URL returned error: 403
1553 │ │ │ │ │ ├─ ⨯ You are not allowed to push code to this project.
1554 │ │ │ │ ╰─ ⨯ run command git push -u origin HEAD
1555 │ │ │ ╰─ ⨯ commit and push
1556 │ │ ╰─ ⨯ bump
1557 │ ╰─ ⨯ bump
1558 │ ├─ ! run id: 019c75a8-e7a6-7bec-822a-e371c363d73e
1559 ╰─ ⨯ cli failed
1560 ");
1561 }
1562
1563 #[test]
1564 fn test_console_macros() {
1565 let writer = MockWriter::new();
1566 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1567
1568 tracing::subscriber::with_default(subscriber, || {
1569 let _span = tracing::info_span!("macros").entered();
1570 console!(notice, "A notice");
1571 console!(info, "Some info");
1572 console!(warn, "A warning");
1573 console!(error, "An error");
1574 });
1575
1576 let output = strip_non_deterministic(&writer.to_string());
1577 insta::assert_snapshot!(output, @r###"
1578 ╭─ ⚙ macros
1579 │ ├─ ! A notice
1580 │ ├─ ✔ Some info
1581 │ ├─ ⚠ A warning
1582 │ ├─ ⨯ An error
1583 ╰─ ⨯ macros failed
1584 "###);
1585 }
1586
1587 #[test]
1588 fn test_console_result() {
1589 let writer = MockWriter::new();
1590 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1591
1592 tracing::subscriber::with_default(subscriber, || {
1593 let _span = tracing::info_span!("result").entered();
1594 let res: Result<&str, &str> = Ok("all good");
1595 assert_eq!(console!(res, "Operation 1").unwrap(), "all good");
1596
1597 let res: Result<&str, &str> = Err("very bad");
1598 assert_eq!(console!(res, "Operation 2").unwrap_err(), "very bad");
1599 });
1600
1601 let output = strip_non_deterministic(&writer.to_string());
1602 insta::assert_snapshot!(output, @r###"
1603 ╭─ ⚙ result
1604 │ ├─ ✔ Operation 1: all good
1605 │ ├─ ⨯ Operation 2: very bad
1606 ╰─ ⨯ result failed
1607 "###);
1608 }
1609
1610 #[test]
1611 fn test_console_result_unit_type() {
1612 let writer = MockWriter::new();
1613 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1614
1615 tracing::subscriber::with_default(subscriber, || {
1616 let _span = tracing::info_span!("result_unit").entered();
1617 let res: Result<(), &str> = Ok(());
1618 console!(res, "Operation without output").unwrap();
1619 });
1620
1621 let output = strip_non_deterministic(&writer.to_string());
1622 insta::assert_snapshot!(output, @r###"
1623 ╭─ ⚙ result_unit
1624 │ ├─ ✔ Operation without output
1625 ╰─ ✔ result_unit succeeded
1626 "###);
1627 }
1628
1629 #[test]
1630 fn test_complex_scenario() {
1631 let writer = MockWriter::new();
1632 let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
1633
1634 tracing::subscriber::with_default(subscriber, || {
1635 let _cli = tracing::info_span!("cli", extra = "cli").entered();
1636 console!(notice, "run id: 928ac7fa-f9d3-4121-9401-b6edbc8369d4");
1637 let _gen = tracing::info_span!("gen").entered();
1638 {
1639 let _config = tracing::info_span!("configuration").entered();
1640 console!(info, "project root folder: /home/user/myapp");
1641 console!(
1642 info,
1643 "configuration loaded from /home/user/myapp/config.toml"
1644 );
1645 }
1646 {
1647 let _templates = tracing::info_span!("templates").entered();
1648 let _render = tracing::info_span!("render package", extra = "cli").entered();
1649 console!(notice, "file /home/user/myapp/build/cli.nix is up to date");
1650 }
1651 {
1652 let _deps = tracing::info_span!("dependencies").entered();
1653 console!(info, "1445 transitive dependencies found");
1654 tracing::error!("something");
1655 }
1656 });
1657
1658 let output = strip_non_deterministic(&writer.to_string());
1659 insta::assert_snapshot!(output, @r"
1660 ╭─ ⚙ cli cli
1661 │ ├─ ! run id: 928ac7fa-f9d3-4121-9401-b6edbc8369d4
1662 │ ╭─ ⚙ gen
1663 │ │ ╭─ ⚙ configuration
1664 │ │ │ ├─ ✔ project root folder: /home/user/myapp
1665 │ │ │ ├─ ✔ configuration loaded from /home/user/myapp/config.toml
1666 │ │ ╰─ ✔ configuration
1667 │ │ ╭─ ⚙ templates
1668 │ │ │ ╭─ ⚙ render package cli
1669 │ │ │ │ ├─ ! file /home/user/myapp/build/cli.nix is up to date
1670 │ │ │ ╰─ ✔ render package cli
1671 │ │ ╰─ ✔ templates
1672 │ │ ╭─ ⚙ dependencies
1673 │ │ │ ├─ ✔ 1445 transitive dependencies found
1674 │ │ │ ├─ ⨯
1675 │ │ ╰─ ⨯ dependencies
1676 │ ╰─ ⨯ gen
1677 ╰─ ⨯ cli cli failed
1678 ");
1679 }
1680
1681 #[test]
1682 fn test_custom_colors() {
1683 use owo_colors::colors;
1684
1685 let writer = MockWriter::new();
1686 let colors = ColorScheme::default()
1687 .with_success_color(colors::BrightGreen)
1688 .with_error_color(colors::BrightRed)
1689 .with_tree_color(colors::Magenta);
1690
1691 let layer = ConsoleLayer::with_writer(writer.clone()).with_colors(colors);
1692 let subscriber = Registry::default().with(layer);
1693
1694 tracing::subscriber::with_default(subscriber, || {
1695 let _span = tracing::info_span!("custom_colors").entered();
1696 console!(info, "Testing custom colors");
1697 console!(error, "This is an error with custom colors");
1698 });
1699
1700 let output = strip_non_deterministic(&writer.to_string());
1701 insta::assert_snapshot!(output, @r###"
1704 ╭─ ⚙ custom_colors
1705 │ ├─ ✔ Testing custom colors
1706 │ ├─ ⨯ This is an error with custom colors
1707 ╰─ ⨯ custom_colors failed
1708 "###);
1709 }
1710
1711 #[test]
1712 fn test_color_scheme_builder() {
1713 let colors = ColorScheme::new()
1715 .with_success_color(owo_colors::colors::Green)
1716 .with_error_color(owo_colors::colors::Red)
1717 .with_warning_color(owo_colors::colors::Yellow)
1718 .with_notice_color(owo_colors::colors::Magenta);
1719
1720 assert!(format!("{:?}", colors).contains("ColorScheme"));
1722 }
1723
1724 #[test]
1725 fn test_colors_with_custom_status() {
1726 #[derive(Clone, Copy)]
1727 struct TestStatusFactory;
1728
1729 impl StatusFactory<DefaultStatus> for TestStatusFactory {
1730 fn group(&self) -> DefaultStatus {
1731 DefaultStatus::Group
1732 }
1733 fn success(&self) -> DefaultStatus {
1734 DefaultStatus::Success
1735 }
1736 fn failure(&self) -> DefaultStatus {
1737 DefaultStatus::Failure
1738 }
1739 }
1740
1741 let writer = MockWriter::new();
1742 let colors = ColorScheme::default().with_success_color(owo_colors::colors::BrightGreen);
1743
1744 let layer = ConsoleLayer::with_writer(writer.clone())
1745 .with_colors(colors)
1746 .with_status(TestStatusFactory);
1747
1748 let subscriber = Registry::default().with(layer);
1749
1750 tracing::subscriber::with_default(subscriber, || {
1751 let _span = tracing::info_span!("test").entered();
1752 console!(info, "Works with custom status factory");
1753 });
1754
1755 let output = strip_non_deterministic(&writer.to_string());
1756 insta::assert_snapshot!(output, @r###"
1757 ╭─ ⚙ test
1758 │ ├─ ✔ Works with custom status factory
1759 ╰─ ✔ test succeeded
1760 "###);
1761 }
1762
1763 #[test]
1764 fn test_format_duration_seconds() {
1765 assert_eq!(format_duration(Duration::from_secs(1)), "1.0s");
1766 assert_eq!(format_duration(Duration::from_millis(1500)), "1.5s");
1767 assert_eq!(format_duration(Duration::from_secs(65)), "65.0s");
1768 assert_eq!(format_duration(Duration::from_secs_f64(1.06)), "1.1s");
1769 }
1770
1771 #[test]
1772 fn test_format_duration_milliseconds() {
1773 assert_eq!(format_duration(Duration::from_millis(1)), "1.0ms");
1774 assert_eq!(format_duration(Duration::from_millis(500)), "500.0ms");
1775 assert_eq!(format_duration(Duration::from_millis(999)), "999.0ms");
1776 assert_eq!(format_duration(Duration::from_micros(1500)), "1.5ms");
1777 }
1778
1779 #[test]
1780 fn test_format_duration_microseconds() {
1781 assert_eq!(format_duration(Duration::from_micros(1)), "1.0µs");
1782 assert_eq!(format_duration(Duration::from_micros(500)), "500.0µs");
1783 assert_eq!(format_duration(Duration::from_micros(999)), "999.0µs");
1784 assert_eq!(format_duration(Duration::from_nanos(1500)), "1.5µs");
1785 }
1786
1787 #[test]
1788 fn test_format_duration_nanoseconds() {
1789 assert_eq!(format_duration(Duration::from_nanos(1)), "1ns");
1790 assert_eq!(format_duration(Duration::from_nanos(500)), "500ns");
1791 assert_eq!(format_duration(Duration::from_nanos(999)), "999ns");
1792 }
1793
1794 #[test]
1795 fn test_format_duration_zero() {
1796 assert_eq!(format_duration(Duration::ZERO), "0ns");
1797 }
1798
1799 #[test]
1800 fn test_format_duration_no_1000_units_at_boundaries() {
1801 assert_eq!(format_duration(Duration::from_nanos(999_999_999)), "1.0s");
1803 assert_eq!(format_duration(Duration::from_nanos(999_950_000)), "1.0s");
1804
1805 assert_eq!(format_duration(Duration::from_nanos(999_999)), "1.0ms");
1807 assert_eq!(format_duration(Duration::from_nanos(999_950)), "1.0ms");
1808
1809 assert_eq!(
1811 format_duration(Duration::from_nanos(999_949_999)),
1812 "999.9ms"
1813 );
1814 assert_eq!(format_duration(Duration::from_nanos(999_949)), "999.9µs");
1815
1816 assert_eq!(format_duration(Duration::from_nanos(1_000_000_000)), "1.0s");
1818 assert_eq!(format_duration(Duration::from_nanos(1_000_000)), "1.0ms");
1819 assert_eq!(format_duration(Duration::from_nanos(1_000)), "1.0µs");
1820 }
1821}