#![cfg_attr(test, allow(clippy::items_after_statements))]
use quilt_rs::logging::{InvalidLogFilter, LOG_ENV};
use std::io;
use tracing::log;
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
mod cli;
use cli::Args;
use cli::Error;
use cli::Std;
use cli::print;
#[tokio::main]
async fn main() {
let args = Args::try_parse_with_env(std::env::args_os()).unwrap_or_else(|err| err.exit());
let logging = init_logging(
std::env::var(LOG_ENV).ok().as_deref(),
args.log_directives(),
);
let format = args.format();
cli::notice_lock_waits(|line| eprintln!("{line}"));
let result = match logging {
Ok(()) => to_std(cli::init(args).await),
Err(err) => Std::Err(err),
};
let failed = matches!(&result, Std::Err(_));
let stdout = io::stdout();
let stderr = io::stderr();
let mut stdout_handle = stdout.lock();
let mut stderr_handle = stderr.lock();
if let Err(err) = print(result, format, &mut stdout_handle, &mut stderr_handle) {
log::error!("Failed to print output: {err}");
std::process::exit(1);
}
if failed {
std::process::exit(1);
}
}
fn to_std(result: Result<Std, Error>) -> Std {
match result {
Ok(result) => result,
Err(err) => Std::Err(err),
}
}
fn init_logging(quilt_log: Option<&str>, flag: Option<String>) -> Result<(), Error> {
let (directives, result) = match filter_directives(quilt_log, flag) {
Ok(directives) => (directives, Ok(())),
Err(err) => (DEFAULT_DIRECTIVES.to_string(), Err(Error::LogEnv(err))),
};
tracing_subscriber::fmt()
.with_env_filter(build_filter(&directives))
.with_writer(io::stderr)
.init();
result
}
const DEFAULT_DIRECTIVES: &str = "warn";
fn build_filter(directives: &str) -> EnvFilter {
EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.parse_lossy(directives)
}
fn filter_directives(
quilt_log: Option<&str>,
flag: Option<String>,
) -> Result<String, InvalidLogFilter> {
let from_env = match quilt_log {
Some(value) => cli::log_filter(value)?,
None => None,
};
Ok(flag
.or(from_env)
.unwrap_or_else(|| DEFAULT_DIRECTIVES.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn ours_at(level: &str) -> String {
format!("quilt={level},quilt_rs={level},quilt_uri={level},quilt_sync={level},warn")
}
fn ok(quilt_log: Option<&str>, flag: Option<&str>) -> String {
filter_directives(quilt_log, flag.map(str::to_string))
.unwrap_or_else(|err| panic!("{quilt_log:?}: {err}"))
}
#[test]
fn without_quilt_log_or_a_flag_everything_is_at_warn() {
assert_eq!(ok(None, None), "warn");
}
#[test]
fn the_flag_sets_the_filter_without_quilt_log() {
assert_eq!(ok(None, Some("quilt_rs=trace")), "quilt_rs=trace");
}
#[test]
fn quilt_log_sets_the_filter_through_the_shared_rule() {
assert_eq!(ok(Some("debug"), None), ours_at("debug"));
assert_eq!(ok(Some("error"), None), "error");
assert_eq!(
ok(Some("quilt_rs=trace,aws_smithy_runtime=debug"), None),
"quilt_rs=trace,aws_smithy_runtime=debug"
);
}
#[test]
fn an_empty_quilt_log_counts_as_unset() {
assert_eq!(ok(Some(""), None), "warn");
assert_eq!(ok(Some(" "), Some("error")), "error");
}
#[test]
fn the_flag_beats_quilt_log() {
assert_eq!(ok(Some("trace"), Some(&ours_at("info"))), ours_at("info"));
assert_eq!(ok(Some("off"), Some(&ours_at("info"))), ours_at("info"));
assert_eq!(
ok(Some("quilt_rs=trace"), Some("hyper=debug")),
"hyper=debug"
);
}
#[test]
fn a_quilt_log_that_does_not_parse_is_an_error() {
for value in [
"debgu",
"quilt_rs",
"quilt_rs=lots",
"quilt_rs=trace,hyper=lots",
] {
assert_eq!(
filter_directives(Some(value), None),
Err(InvalidLogFilter::new(value)),
"{value}"
);
}
}
#[test]
fn a_quilt_log_that_does_not_parse_is_an_error_with_a_flag_too() {
assert_eq!(
filter_directives(Some("debgu"), Some(ours_at("info"))),
Err(InvalidLogFilter::new("debgu"))
);
}
fn printed(err: InvalidLogFilter, format: cli::Format) -> String {
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
print(
Std::Err(Error::LogEnv(err)),
format,
&mut stdout,
&mut stderr,
)
.unwrap();
assert_eq!(stdout, b"");
String::from_utf8(stderr).unwrap()
}
#[test]
fn an_invalid_quilt_log_prints_as_the_json_error_object() {
let json: serde_json::Value =
serde_json::from_str(&printed(InvalidLogFilter::new("debgu"), cli::Format::Json))
.unwrap();
assert_eq!(json["error"]["kind"], "invalid_log_filter");
assert_eq!(
json["error"]["message"],
"QUILT_LOG=\"debgu\" is not a log level or a list of directives"
);
}
#[test]
fn an_invalid_quilt_log_prints_as_text() {
assert_eq!(
printed(InvalidLogFilter::new("debgu"), cli::Format::Text),
"QUILT_LOG=\"debgu\" is not a log level or a list of directives\n"
);
}
#[test]
fn a_bare_level_keeps_dependencies_at_warn() {
let subscriber = tracing_subscriber::fmt()
.with_env_filter(build_filter(&ours_at("debug")))
.with_writer(io::sink)
.finish();
tracing::subscriber::with_default(subscriber, || {
assert!(tracing::enabled!(target: "quilt_rs::flow", tracing::Level::DEBUG));
assert!(tracing::enabled!(target: "quilt", tracing::Level::DEBUG));
assert!(!tracing::enabled!(target: "quilt_rs", tracing::Level::TRACE));
assert!(!tracing::enabled!(target: "hyper", tracing::Level::DEBUG));
assert!(tracing::enabled!(target: "hyper", tracing::Level::WARN));
});
}
#[test]
fn to_std_routes_pre_dispatch_error_the_same_as_a_command_error() {
assert!(matches!(
to_std(Err(Error::Domain)),
Std::Err(Error::Domain)
));
}
#[test]
fn to_std_passes_a_successful_init_through_unchanged() {
assert!(matches!(
to_std(Ok(Std::Err(Error::Home))),
Std::Err(Error::Home)
));
}
}