use std::io::{self, Write};
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use owo_colors::{OwoColorize, Style};
use tracing::field::Visit;
use tracing::{Event, Id};
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::registry::LookupSpan;
#[derive(Clone, Debug)]
pub struct ColorScheme {
pub success: Style,
pub error: Style,
pub warning: Style,
pub notice: Style,
pub group: Style,
pub tree: Style,
pub extra: Style,
pub succeeded: Style,
pub failed: Style,
pub duration: Style,
}
impl Default for ColorScheme {
fn default() -> Self {
Self {
success: Style::new().green(),
error: Style::new().red(),
warning: Style::new().yellow(),
notice: Style::new().purple(),
group: Style::new().dimmed(),
tree: Style::new().blue(),
extra: Style::new().cyan(),
succeeded: Style::new().green().bold(),
failed: Style::new().red().bold(),
duration: Style::new().dimmed(),
}
}
}
impl ColorScheme {
pub fn new() -> Self {
Self::default()
}
pub fn with_success_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.success = Style::new().fg::<C>();
self
}
pub fn with_success_style(mut self, style: Style) -> Self {
self.success = style;
self
}
pub fn with_error_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.error = Style::new().fg::<C>();
self
}
pub fn with_error_style(mut self, style: Style) -> Self {
self.error = style;
self
}
pub fn with_warning_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.warning = Style::new().fg::<C>();
self
}
pub fn with_warning_style(mut self, style: Style) -> Self {
self.warning = style;
self
}
pub fn with_notice_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.notice = Style::new().fg::<C>();
self
}
pub fn with_notice_style(mut self, style: Style) -> Self {
self.notice = style;
self
}
pub fn with_group_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.group = Style::new().fg::<C>();
self
}
pub fn with_group_style(mut self, style: Style) -> Self {
self.group = style;
self
}
pub fn with_tree_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.tree = Style::new().fg::<C>();
self
}
pub fn with_tree_style(mut self, style: Style) -> Self {
self.tree = style;
self
}
pub fn with_extra_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.extra = Style::new().fg::<C>();
self
}
pub fn with_extra_style(mut self, style: Style) -> Self {
self.extra = style;
self
}
pub fn with_succeeded_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.succeeded = Style::new().fg::<C>().bold();
self
}
pub fn with_succeeded_style(mut self, style: Style) -> Self {
self.succeeded = style;
self
}
pub fn with_failed_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.failed = Style::new().fg::<C>().bold();
self
}
pub fn with_failed_style(mut self, style: Style) -> Self {
self.failed = style;
self
}
pub fn with_duration_color<C: owo_colors::Color>(mut self, _color: C) -> Self {
self.duration = Style::new().fg::<C>();
self
}
pub fn with_duration_style(mut self, style: Style) -> Self {
self.duration = style;
self
}
}
pub trait Status: Send + Sync {
fn from_str(s: &str) -> Self;
fn write_icon<W: Write>(&self, out: W, colors: &ColorScheme) -> io::Result<()>;
fn is_passthrough(&self) -> bool;
}
pub trait StatusFactory<S: Status>: Send + Sync {
fn group(&self) -> S;
fn success(&self) -> S;
fn failure(&self) -> S;
}
#[derive(Debug, Clone, Copy)]
pub enum DefaultStatus {
Notice,
Success,
Failure,
Warning,
Group,
Pass,
}
impl Status for DefaultStatus {
fn from_str(s: &str) -> Self {
match s {
"notice" => Self::Notice,
"success" => Self::Success,
"warn" => Self::Warning,
"error" => Self::Failure,
"pass" => Self::Pass,
_ => Self::Success,
}
}
fn write_icon<W: Write>(&self, mut out: W, colors: &ColorScheme) -> io::Result<()> {
match self {
Self::Notice => write!(out, "{}", colors.notice.style("!")),
Self::Success => write!(out, "{}", colors.success.style("✔")),
Self::Failure => write!(out, "{}", colors.error.style("⨯")),
Self::Warning => write!(out, "{}", colors.warning.style("⚠")),
Self::Group => write!(out, "{}", colors.group.style("⚙")),
Self::Pass => write!(out, ""),
}
}
fn is_passthrough(&self) -> bool {
matches!(self, Self::Pass)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultStatusFactory;
impl StatusFactory<DefaultStatus> for DefaultStatusFactory {
fn group(&self) -> DefaultStatus {
DefaultStatus::Group
}
fn success(&self) -> DefaultStatus {
DefaultStatus::Success
}
fn failure(&self) -> DefaultStatus {
DefaultStatus::Failure
}
}
fn format_duration(d: Duration) -> String {
let nanos = d.as_nanos();
if nanos >= 999_950_000 {
format!("{:.1}s", d.as_secs_f64())
} else if nanos >= 999_950 {
format!("{:.1}ms", nanos as f64 / 1_000_000.0)
} else if nanos >= 1_000 {
format!("{:.1}µs", nanos as f64 / 1_000.0)
} else {
format!("{nanos}ns")
}
}
#[derive(Debug)]
pub struct ConsoleLayer<W, F = DefaultStatusFactory, S = DefaultStatus> {
make_writer: W,
status_factory: F,
colors: Arc<ColorScheme>,
_status: PhantomData<S>,
}
impl Default for ConsoleLayer<fn() -> io::Stderr> {
fn default() -> Self {
Self {
make_writer: io::stderr,
status_factory: DefaultStatusFactory,
colors: Arc::new(ColorScheme::default()),
_status: PhantomData,
}
}
}
impl<W> ConsoleLayer<W> {
pub fn with_writer(make_writer: W) -> Self {
Self {
make_writer,
status_factory: DefaultStatusFactory,
colors: Arc::new(ColorScheme::default()),
_status: PhantomData,
}
}
}
impl<W, F, S> ConsoleLayer<W, F, S> {
const HEADER_START: &str = "╭─";
const FOOTER_START: &str = "╰─";
const GROUP_START: &str = "╭─";
const GROUP_END: &str = "╰─";
const ITEM_START: &str = "├─";
const PIPE: &str = "│ ";
const PIPE_PREFIX: &str = "│ ";
const CLOCK: &str = "⏱";
pub fn with_status<CustomF, CustomS>(
self,
status_factory: CustomF,
) -> ConsoleLayer<W, CustomF, CustomS>
where
CustomF: StatusFactory<CustomS>,
CustomS: Status,
{
ConsoleLayer {
make_writer: self.make_writer,
status_factory,
colors: self.colors,
_status: PhantomData,
}
}
pub fn with_colors(mut self, colors: ColorScheme) -> Self {
self.colors = Arc::new(colors);
self
}
fn print_line<St: Status>(
&self,
depth: usize,
start_symbol: &str,
status: St,
message: &str,
duration: Option<Duration>,
bold: bool,
) where
W: for<'writer> MakeWriter<'writer>,
{
let mut out = self.make_writer.make_writer();
if depth > 0 {
write!(out, "{}", self.colors.tree.style(Self::PIPE)).ok();
for _ in 1..depth {
write!(out, "{}", self.colors.tree.style(Self::PIPE_PREFIX)).ok();
}
}
write!(out, "{}", self.colors.tree.style(start_symbol)).ok();
write!(out, " ").ok();
status.write_icon(&mut out, &self.colors).ok();
if bold {
write!(out, " {}", message.bold()).ok();
} else {
write!(out, " {}", message).ok();
}
if let Some(d) = duration {
write!(
out,
" {}",
self.colors
.duration
.style(format!("{} {} ", Self::CLOCK, format_duration(d)))
)
.ok();
}
writeln!(out).ok();
}
}
#[derive(Debug)]
struct ConsoleSpanInfo {
extra: Option<String>,
has_failed: AtomicBool,
}
impl<S, W, F, St> Layer<S> for ConsoleLayer<W, F, St>
where
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
W: for<'writer> MakeWriter<'writer> + 'static + Send + Sync,
F: StatusFactory<St> + 'static,
St: Status + 'static,
{
fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id) {
let mut visitor = FieldVisitor::default();
attrs.record(&mut visitor);
span.extensions_mut().insert(ConsoleSpanInfo {
extra: visitor.extra,
has_failed: AtomicBool::new(false),
});
}
}
fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(id) {
if span.extensions().get::<Instant>().is_some() {
return;
}
span.extensions_mut().insert(Instant::now());
let metadata = span.metadata();
let extensions = span.extensions();
let info = extensions.get::<ConsoleSpanInfo>().unwrap();
let depth = span.scope().from_root().count() - 1;
let start_symbol = if depth == 0 {
Self::HEADER_START
} else {
Self::GROUP_START
};
let message = if let Some(extra) = &info.extra {
format!("{} {}", metadata.name(), self.colors.extra.style(extra))
} else {
metadata.name().to_string()
};
self.print_line(
depth,
start_symbol,
self.status_factory.group(),
&message,
None,
true,
);
}
}
fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
let mut visitor = FieldVisitor::default();
event.record(&mut visitor);
let is_failure = *event.metadata().level() == tracing::Level::ERROR;
let mut message = visitor.message.clone();
let was_already_failed = if is_failure && !visitor.console {
ctx.event_scope(event)
.and_then(|mut scope| scope.next())
.and_then(|span| {
span.extensions()
.get::<ConsoleSpanInfo>()
.map(|info| info.has_failed.load(Ordering::SeqCst))
})
.unwrap_or(false)
} else {
false
};
if is_failure {
if let Some(scope) = ctx.event_scope(event) {
for span in scope.from_root() {
if let Some(info) = span.extensions().get::<ConsoleSpanInfo>() {
info.has_failed.store(true, Ordering::SeqCst);
}
}
}
if let Some(error) = visitor.error {
if message.is_empty() {
message = error;
} else {
message = format!("{message}: {error}");
}
}
}
if visitor.console || (is_failure && !was_already_failed) {
let status = if visitor.console {
St::from_str(&visitor.status)
} else {
self.status_factory.failure()
};
let (start_symbol, bold) = if status.is_passthrough() {
("", false)
} else {
(Self::ITEM_START, true)
};
let depth = ctx.event_scope(event).map_or(0, |scope| scope.count());
self.print_line(depth, start_symbol, status, &message, None, bold);
}
}
fn on_close(&self, id: Id, ctx: Context<'_, S>) {
if let Some(span) = ctx.span(&id) {
let extensions = span.extensions();
let info = extensions.get::<ConsoleSpanInfo>().unwrap();
let duration = extensions
.get::<Instant>()
.map(|start_time| start_time.elapsed());
let metadata = span.metadata();
let has_failed = info.has_failed.load(Ordering::SeqCst);
let status = if has_failed {
self.status_factory.failure()
} else {
self.status_factory.success()
};
let base_message = if let Some(extra) = &info.extra {
format!("{} {}", metadata.name(), self.colors.extra.style(extra))
} else {
metadata.name().to_string()
};
let depth = span.scope().from_root().count() - 1;
let (final_message, start_symbol) = if depth == 0 {
let status_string = if has_failed {
self.colors.failed.style("failed").to_string()
} else {
self.colors.succeeded.style("succeeded").to_string()
};
(
format!("{base_message} {status_string}"),
Self::FOOTER_START,
)
} else {
(base_message, Self::GROUP_END)
};
self.print_line(depth, start_symbol, status, &final_message, duration, true);
}
}
}
#[derive(Default)]
struct FieldVisitor {
status: String,
message: String,
console: bool,
extra: Option<String>,
error: Option<String>,
}
impl Visit for FieldVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
match field.name() {
"message" => self.message = value.to_string(),
"status" => self.status = value.to_string(),
"extra" => self.extra = Some(value.to_string()),
_ => {}
}
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
if let "console" = field.name() {
self.console = value
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "error" {
self.error = Some(format!("{value:?}"));
}
}
}
#[doc(hidden)]
pub mod macros {
use owo_colors::OwoColorize;
use std::fmt::Display;
pub struct Displayable<T>(pub T);
pub trait ConsoleResult<T> {
fn format_success(self, colorize: impl Fn(String) -> String, message: String) -> String;
}
impl ConsoleResult<()> for () {
fn format_success(self, _colorize: impl Fn(String) -> String, message: String) -> String {
message
}
}
impl<T: Display> ConsoleResult<Displayable<T>> for T {
fn format_success(self, colorize: impl Fn(String) -> String, message: String) -> String {
let this = self.to_string();
if this.is_empty() {
message
} else {
format!("{}: {}", message, colorize(self.to_string()))
}
}
}
pub fn format_failure<E: Display>(err: &E, message: String) -> String {
format!("{}: {}", message, err.to_string().red())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __console_impl_result {
($result:expr, $colorize:expr, $($arg:tt)+) => {
match $result {
Ok(result) => {
use $crate::macros::ConsoleResult;
let message = result.format_success($colorize, format!($($arg)+));
tracing::event!(
tracing::Level::INFO,
console = true,
status = "success",
message = message
);
Ok(result)
}
Err(e) => {
let error_message = $crate::macros::format_failure(&e, format!($($arg)+));
tracing::event!(
tracing::Level::ERROR,
console = true,
status = "error",
message = error_message
);
Err(e)
}
}
};
}
#[macro_export]
macro_rules! console {
(pass, $($arg:tt)+) => {
tracing::event!(tracing::Level::INFO, console = true, status = "pass", message = &format!($($arg)+));
};
(notice, $($arg:tt)+) => {
tracing::event!(tracing::Level::INFO, console = true, status = "notice", message = &format!($($arg)+));
};
(info, $($arg:tt)+) => {
tracing::event!(tracing::Level::INFO, console = true, status = "success", message = &format!($($arg)+));
};
(warn, $($arg:tt)+) => {
tracing::event!(tracing::Level::WARN, console = true, status = "warn", message = &format!($($arg)+));
};
(error, $($arg:tt)+) => {
tracing::event!(tracing::Level::ERROR, console = true, status = "error", message = &format!($($arg)+));
};
($result:expr, $color:expr, $($arg:tt)+) => {
$crate::__console_impl_result!($result, $color, $($arg)+)
};
($result:expr, $($arg:tt)+) => {
$crate::__console_impl_result!($result, |s: String| s.green().to_string(), $($arg)+)
};
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
use std::fmt::Display;
use std::sync::{Arc, Mutex};
use tracing_subscriber::Registry;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::prelude::*;
#[derive(Debug, Clone)]
struct MockWriter {
buf: Arc<Mutex<Vec<u8>>>,
}
impl Display for MockWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
String::from_utf8(self.buf.lock().unwrap().clone()).unwrap()
)
}
}
impl MockWriter {
fn new() -> Self {
Self {
buf: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl<'a> MakeWriter<'a> for MockWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
impl io::Write for MockWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buf.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.buf.lock().unwrap().flush()
}
}
fn strip_non_deterministic(output: &str) -> String {
let output = strip_ansi_escapes::strip_str(output);
let duration_regex = Regex::new(r" ⏱ .*? ").unwrap();
duration_regex.replace_all(&output, "").trim().to_string()
}
#[test]
fn test_simple_span() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("simple").entered();
console!(info, "It works!");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ simple
│ ├─ ✔ It works!
╰─ ✔ simple succeeded
"###);
}
#[test]
fn test_nested_spans() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _outer = tracing::info_span!("outer").entered();
let _inner = tracing::info_span!("inner").entered();
console!(notice, "Something is happening");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ outer
│ ╭─ ⚙ inner
│ │ ├─ ! Something is happening
│ ╰─ ✔ inner
╰─ ✔ outer succeeded
"###);
}
#[test]
fn test_error_propagation() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
#[tracing::instrument(name = "inner", err)]
fn inner() -> Result<(), &'static str> {
Err("An error occurred")
}
let _outer = tracing::info_span!("outer").entered();
inner().ok()
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r"
╭─ ⚙ outer
│ ╭─ ⚙ inner
│ │ ├─ ⨯ An error occurred
│ ╰─ ⨯ inner
╰─ ⨯ outer failed
");
}
#[test]
fn test_nested_instrument_err_deduplication() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
#[tracing::instrument(name = "inner", err)]
fn inner() -> Result<(), &'static str> {
Err("something went wrong")
}
#[tracing::instrument(name = "middle", err)]
fn middle() -> Result<(), &'static str> {
inner()
}
#[tracing::instrument(name = "outer", err)]
fn outer() -> Result<(), &'static str> {
middle()
}
let _root = tracing::info_span!("root").entered();
outer().ok();
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r"
╭─ ⚙ root
│ ╭─ ⚙ outer
│ │ ╭─ ⚙ middle
│ │ │ ╭─ ⚙ inner
│ │ │ │ ├─ ⨯ something went wrong
│ │ │ ╰─ ⨯ inner
│ │ ╰─ ⨯ middle
│ ╰─ ⨯ outer
╰─ ⨯ root failed
");
}
#[test]
fn test_bump_branch_already_exists() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
#[tracing::instrument(name = "run command git checkout -b bump/v0.15.4", err)]
fn git_checkout_new_branch() -> Result<(), String> {
console!(warn, "fatal: a branch named 'bump/v0.15.4' already exists");
Err(
"error running command git checkout -b bump/v0.15.4, exit code: Some(128)"
.to_string(),
)
}
#[tracing::instrument(name = "bump", err)]
fn bump_inner() -> Result<(), String> {
{
let _span = tracing::info_span!("configure git").entered();
{
let _cmd =
tracing::info_span!("run command git config user.name").entered();
}
{
let _cmd =
tracing::info_span!("run command git config user.email").entered();
}
}
{
let _span = tracing::info_span!("fetch and checkout").entered();
{
let _cmd =
tracing::info_span!("run command git fetch origin develop").entered();
}
{
let _cmd = tracing::info_span!("run command git checkout origin/develop")
.entered();
}
}
console!(info, "current version: 0.15.3");
console!(info, "new version: 0.15.4");
{
let _cmd = tracing::info_span!(
"run command git ls-remote --exit-code --heads origin bump/v0.15.4"
)
.entered();
}
git_checkout_new_branch()
}
#[tracing::instrument(name = "bump", err)]
fn bump_outer() -> Result<(), String> {
bump_inner()
}
let _cli = tracing::info_span!("cli").entered();
console!(notice, "run id: 019c7593-1f68-784a-ade1-9a06a33a38be");
bump_outer().ok();
console!(notice, "run id: 019c7593-1f68-784a-ade1-9a06a33a38be");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r"
╭─ ⚙ cli
│ ├─ ! run id: 019c7593-1f68-784a-ade1-9a06a33a38be
│ ╭─ ⚙ bump
│ │ ╭─ ⚙ bump
│ │ │ ╭─ ⚙ configure git
│ │ │ │ ╭─ ⚙ run command git config user.name
│ │ │ │ ╰─ ✔ run command git config user.name
│ │ │ │ ╭─ ⚙ run command git config user.email
│ │ │ │ ╰─ ✔ run command git config user.email
│ │ │ ╰─ ✔ configure git
│ │ │ ╭─ ⚙ fetch and checkout
│ │ │ │ ╭─ ⚙ run command git fetch origin develop
│ │ │ │ ╰─ ✔ run command git fetch origin develop
│ │ │ │ ╭─ ⚙ run command git checkout origin/develop
│ │ │ │ ╰─ ✔ run command git checkout origin/develop
│ │ │ ╰─ ✔ fetch and checkout
│ │ │ ├─ ✔ current version: 0.15.3
│ │ │ ├─ ✔ new version: 0.15.4
│ │ │ ╭─ ⚙ run command git ls-remote --exit-code --heads origin bump/v0.15.4
│ │ │ ╰─ ✔ run command git ls-remote --exit-code --heads origin bump/v0.15.4
│ │ │ ╭─ ⚙ run command git checkout -b bump/v0.15.4
│ │ │ │ ├─ ⚠ fatal: a branch named 'bump/v0.15.4' already exists
│ │ │ │ ├─ ⨯ error running command git checkout -b bump/v0.15.4, exit code: Some(128)
│ │ │ ╰─ ⨯ run command git checkout -b bump/v0.15.4
│ │ ╰─ ⨯ bump
│ ╰─ ⨯ bump
│ ├─ ! run id: 019c7593-1f68-784a-ade1-9a06a33a38be
╰─ ⨯ cli failed
");
}
#[test]
fn test_bump_push_permission_denied() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
#[tracing::instrument(name = "run command git push -u origin HEAD", err)]
fn git_push() -> Result<(), String> {
console!(
warn,
"remote: You are not allowed to push code to this project."
);
console!(
warn,
"fatal: unable to access 'https://git.example.com/example/project.git/': The requested URL returned error: 403"
);
Err("You are not allowed to push code to this project.".to_string())
}
#[tracing::instrument(name = "commit and push", err)]
fn commit_and_push() -> Result<(), String> {
{
let _cmd =
tracing::info_span!("run command git add Cargo.toml Cargo.lock").entered();
}
{
let _cmd =
tracing::info_span!("run command git commit -m bump version to 0.15.4")
.entered();
}
git_push()
}
#[tracing::instrument(name = "bump", err)]
fn bump_inner() -> Result<(), String> {
{
let _span = tracing::info_span!("configure git").entered();
}
{
let _span = tracing::info_span!("fetch and checkout").entered();
}
console!(info, "current version: 0.15.3");
console!(info, "new version: 0.15.4");
{
let _cmd =
tracing::info_span!("run command git checkout -b bump/v0.15.4").entered();
}
{
let _span = tracing::info_span!("update cargo.toml").entered();
console!(info, "updated Cargo.toml to version 0.15.4");
}
{
let _span = tracing::info_span!("update cargo.lock").entered();
{
let _cmd =
tracing::info_span!("run command cargo update --workspace").entered();
}
console!(info, "updated Cargo.lock");
}
commit_and_push()
}
#[tracing::instrument(name = "bump", err)]
fn bump_outer() -> Result<(), String> {
bump_inner()
}
let _cli = tracing::info_span!("cli").entered();
console!(notice, "run id: 019c75a8-e7a6-7bec-822a-e371c363d73e");
bump_outer().ok();
console!(notice, "run id: 019c75a8-e7a6-7bec-822a-e371c363d73e");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r"
╭─ ⚙ cli
│ ├─ ! run id: 019c75a8-e7a6-7bec-822a-e371c363d73e
│ ╭─ ⚙ bump
│ │ ╭─ ⚙ bump
│ │ │ ╭─ ⚙ configure git
│ │ │ ╰─ ✔ configure git
│ │ │ ╭─ ⚙ fetch and checkout
│ │ │ ╰─ ✔ fetch and checkout
│ │ │ ├─ ✔ current version: 0.15.3
│ │ │ ├─ ✔ new version: 0.15.4
│ │ │ ╭─ ⚙ run command git checkout -b bump/v0.15.4
│ │ │ ╰─ ✔ run command git checkout -b bump/v0.15.4
│ │ │ ╭─ ⚙ update cargo.toml
│ │ │ │ ├─ ✔ updated Cargo.toml to version 0.15.4
│ │ │ ╰─ ✔ update cargo.toml
│ │ │ ╭─ ⚙ update cargo.lock
│ │ │ │ ╭─ ⚙ run command cargo update --workspace
│ │ │ │ ╰─ ✔ run command cargo update --workspace
│ │ │ │ ├─ ✔ updated Cargo.lock
│ │ │ ╰─ ✔ update cargo.lock
│ │ │ ╭─ ⚙ commit and push
│ │ │ │ ╭─ ⚙ run command git add Cargo.toml Cargo.lock
│ │ │ │ ╰─ ✔ run command git add Cargo.toml Cargo.lock
│ │ │ │ ╭─ ⚙ run command git commit -m bump version to 0.15.4
│ │ │ │ ╰─ ✔ run command git commit -m bump version to 0.15.4
│ │ │ │ ╭─ ⚙ run command git push -u origin HEAD
│ │ │ │ │ ├─ ⚠ remote: You are not allowed to push code to this project.
│ │ │ │ │ ├─ ⚠ fatal: unable to access 'https://git.example.com/example/project.git/': The requested URL returned error: 403
│ │ │ │ │ ├─ ⨯ You are not allowed to push code to this project.
│ │ │ │ ╰─ ⨯ run command git push -u origin HEAD
│ │ │ ╰─ ⨯ commit and push
│ │ ╰─ ⨯ bump
│ ╰─ ⨯ bump
│ ├─ ! run id: 019c75a8-e7a6-7bec-822a-e371c363d73e
╰─ ⨯ cli failed
");
}
#[test]
fn test_console_macros() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("macros").entered();
console!(notice, "A notice");
console!(info, "Some info");
console!(warn, "A warning");
console!(error, "An error");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ macros
│ ├─ ! A notice
│ ├─ ✔ Some info
│ ├─ ⚠ A warning
│ ├─ ⨯ An error
╰─ ⨯ macros failed
"###);
}
#[test]
fn test_console_result() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("result").entered();
let res: Result<&str, &str> = Ok("all good");
assert_eq!(console!(res, "Operation 1").unwrap(), "all good");
let res: Result<&str, &str> = Err("very bad");
assert_eq!(console!(res, "Operation 2").unwrap_err(), "very bad");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ result
│ ├─ ✔ Operation 1: all good
│ ├─ ⨯ Operation 2: very bad
╰─ ⨯ result failed
"###);
}
#[test]
fn test_console_result_unit_type() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("result_unit").entered();
let res: Result<(), &str> = Ok(());
console!(res, "Operation without output").unwrap();
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ result_unit
│ ├─ ✔ Operation without output
╰─ ✔ result_unit succeeded
"###);
}
#[test]
fn test_complex_scenario() {
let writer = MockWriter::new();
let subscriber = Registry::default().with(ConsoleLayer::with_writer(writer.clone()));
tracing::subscriber::with_default(subscriber, || {
let _cli = tracing::info_span!("cli", extra = "cli").entered();
console!(notice, "run id: 928ac7fa-f9d3-4121-9401-b6edbc8369d4");
let _gen = tracing::info_span!("gen").entered();
{
let _config = tracing::info_span!("configuration").entered();
console!(info, "project root folder: /home/user/myapp");
console!(
info,
"configuration loaded from /home/user/myapp/config.toml"
);
}
{
let _templates = tracing::info_span!("templates").entered();
let _render = tracing::info_span!("render package", extra = "cli").entered();
console!(notice, "file /home/user/myapp/build/cli.nix is up to date");
}
{
let _deps = tracing::info_span!("dependencies").entered();
console!(info, "1445 transitive dependencies found");
tracing::error!("something");
}
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r"
╭─ ⚙ cli cli
│ ├─ ! run id: 928ac7fa-f9d3-4121-9401-b6edbc8369d4
│ ╭─ ⚙ gen
│ │ ╭─ ⚙ configuration
│ │ │ ├─ ✔ project root folder: /home/user/myapp
│ │ │ ├─ ✔ configuration loaded from /home/user/myapp/config.toml
│ │ ╰─ ✔ configuration
│ │ ╭─ ⚙ templates
│ │ │ ╭─ ⚙ render package cli
│ │ │ │ ├─ ! file /home/user/myapp/build/cli.nix is up to date
│ │ │ ╰─ ✔ render package cli
│ │ ╰─ ✔ templates
│ │ ╭─ ⚙ dependencies
│ │ │ ├─ ✔ 1445 transitive dependencies found
│ │ │ ├─ ⨯
│ │ ╰─ ⨯ dependencies
│ ╰─ ⨯ gen
╰─ ⨯ cli cli failed
");
}
#[test]
fn test_custom_colors() {
use owo_colors::colors;
let writer = MockWriter::new();
let colors = ColorScheme::default()
.with_success_color(colors::BrightGreen)
.with_error_color(colors::BrightRed)
.with_tree_color(colors::Magenta);
let layer = ConsoleLayer::with_writer(writer.clone()).with_colors(colors);
let subscriber = Registry::default().with(layer);
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("custom_colors").entered();
console!(info, "Testing custom colors");
console!(error, "This is an error with custom colors");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ custom_colors
│ ├─ ✔ Testing custom colors
│ ├─ ⨯ This is an error with custom colors
╰─ ⨯ custom_colors failed
"###);
}
#[test]
fn test_color_scheme_builder() {
let colors = ColorScheme::new()
.with_success_color(owo_colors::colors::Green)
.with_error_color(owo_colors::colors::Red)
.with_warning_color(owo_colors::colors::Yellow)
.with_notice_color(owo_colors::colors::Magenta);
assert!(format!("{:?}", colors).contains("ColorScheme"));
}
#[test]
fn test_colors_with_custom_status() {
#[derive(Clone, Copy)]
struct TestStatusFactory;
impl StatusFactory<DefaultStatus> for TestStatusFactory {
fn group(&self) -> DefaultStatus {
DefaultStatus::Group
}
fn success(&self) -> DefaultStatus {
DefaultStatus::Success
}
fn failure(&self) -> DefaultStatus {
DefaultStatus::Failure
}
}
let writer = MockWriter::new();
let colors = ColorScheme::default().with_success_color(owo_colors::colors::BrightGreen);
let layer = ConsoleLayer::with_writer(writer.clone())
.with_colors(colors)
.with_status(TestStatusFactory);
let subscriber = Registry::default().with(layer);
tracing::subscriber::with_default(subscriber, || {
let _span = tracing::info_span!("test").entered();
console!(info, "Works with custom status factory");
});
let output = strip_non_deterministic(&writer.to_string());
insta::assert_snapshot!(output, @r###"
╭─ ⚙ test
│ ├─ ✔ Works with custom status factory
╰─ ✔ test succeeded
"###);
}
#[test]
fn test_format_duration_seconds() {
assert_eq!(format_duration(Duration::from_secs(1)), "1.0s");
assert_eq!(format_duration(Duration::from_millis(1500)), "1.5s");
assert_eq!(format_duration(Duration::from_secs(65)), "65.0s");
assert_eq!(format_duration(Duration::from_secs_f64(1.06)), "1.1s");
}
#[test]
fn test_format_duration_milliseconds() {
assert_eq!(format_duration(Duration::from_millis(1)), "1.0ms");
assert_eq!(format_duration(Duration::from_millis(500)), "500.0ms");
assert_eq!(format_duration(Duration::from_millis(999)), "999.0ms");
assert_eq!(format_duration(Duration::from_micros(1500)), "1.5ms");
}
#[test]
fn test_format_duration_microseconds() {
assert_eq!(format_duration(Duration::from_micros(1)), "1.0µs");
assert_eq!(format_duration(Duration::from_micros(500)), "500.0µs");
assert_eq!(format_duration(Duration::from_micros(999)), "999.0µs");
assert_eq!(format_duration(Duration::from_nanos(1500)), "1.5µs");
}
#[test]
fn test_format_duration_nanoseconds() {
assert_eq!(format_duration(Duration::from_nanos(1)), "1ns");
assert_eq!(format_duration(Duration::from_nanos(500)), "500ns");
assert_eq!(format_duration(Duration::from_nanos(999)), "999ns");
}
#[test]
fn test_format_duration_zero() {
assert_eq!(format_duration(Duration::ZERO), "0ns");
}
#[test]
fn test_format_duration_no_1000_units_at_boundaries() {
assert_eq!(format_duration(Duration::from_nanos(999_999_999)), "1.0s");
assert_eq!(format_duration(Duration::from_nanos(999_950_000)), "1.0s");
assert_eq!(format_duration(Duration::from_nanos(999_999)), "1.0ms");
assert_eq!(format_duration(Duration::from_nanos(999_950)), "1.0ms");
assert_eq!(
format_duration(Duration::from_nanos(999_949_999)),
"999.9ms"
);
assert_eq!(format_duration(Duration::from_nanos(999_949)), "999.9µs");
assert_eq!(format_duration(Duration::from_nanos(1_000_000_000)), "1.0s");
assert_eq!(format_duration(Duration::from_nanos(1_000_000)), "1.0ms");
assert_eq!(format_duration(Duration::from_nanos(1_000)), "1.0µs");
}
}