pub mod console;
mod file;
mod panic_hook;
use std::cell::RefCell;
use std::env;
use std::fmt;
use std::io::{self, Write as _};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant};
use ::log::{Log, Metadata, Record, SetLoggerError};
use crcbl_console::Fault;
pub use console::{CONSOLE_RING_LINES, CONSOLE_TARGET};
use file::FileSink;
pub use file::{FILE_ENV_VAR, LOG_FILE_MAX_BYTES, LOG_FILES_KEPT};
pub use ::log::{Level, LevelFilter};
pub use crate::{debug, error, info, log_at, trace, warn};
pub const ENV_VAR: &str = "CRCBL_LOG";
pub const DEFAULT_FILTER: &str = "info";
#[derive(Clone, Debug)]
pub struct Filter {
default: LevelFilter,
targets: Vec<(String, LevelFilter)>,
}
impl Filter {
const INITIAL: Self = Self {
default: LevelFilter::Info,
targets: Vec::new(),
};
#[must_use]
pub fn parse(directives: &str) -> Self {
Self::read(directives).0
}
pub fn try_parse(directives: &str) -> Result<Self, BadDirective> {
let (filter, refused) = Self::read(directives);
match refused.into_iter().next() {
Some(directive) => Err(BadDirective(directive)),
None => Ok(filter),
}
}
fn read(directives: &str) -> (Self, Vec<String>) {
let mut filter = Self {
default: parse_level(DEFAULT_FILTER).unwrap_or(LevelFilter::Info),
targets: Vec::new(),
};
let mut refused = Vec::new();
for directive in directives.split(',') {
let directive = directive.trim();
if directive.is_empty() {
continue;
}
match directive.split_once('=') {
Some((target, level)) => {
if let Some(level) = parse_level(level.trim()) {
filter.targets.push((target.trim().to_owned(), level));
} else {
refused.push(directive.to_owned());
}
}
None => {
if let Some(level) = parse_level(directive) {
filter.default = level;
} else {
refused.push(directive.to_owned());
}
}
}
}
filter
.targets
.sort_by_key(|(target, _)| core::cmp::Reverse(target.len()));
(filter, refused)
}
#[must_use]
pub fn from_env() -> Self {
match env::var(ENV_VAR) {
Ok(value) if !value.trim().is_empty() => Self::parse(&value),
_ => Self::parse(DEFAULT_FILTER),
}
}
#[must_use]
pub fn level_for(&self, target: &str) -> LevelFilter {
self.targets
.iter()
.find(|(prefix, _)| target_matches(target, prefix))
.map_or(self.default, |(_, level)| *level)
}
fn permits(&self, level: Level, target: &str) -> bool {
level <= self.level_for(target)
}
#[must_use]
pub fn max_level(&self) -> LevelFilter {
self.targets
.iter()
.map(|(_, level)| *level)
.chain([self.default])
.max()
.unwrap_or(LevelFilter::Off)
}
}
impl Default for Filter {
fn default() -> Self {
Self::parse(DEFAULT_FILTER)
}
}
impl fmt::Display for Filter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(level_directive(self.default))?;
for (target, level) in &self.targets {
write!(f, ",{target}={}", level_directive(*level))?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BadDirective(String);
impl BadDirective {
#[must_use]
pub fn directive(&self) -> &str {
&self.0
}
}
impl fmt::Display for BadDirective {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`{}` is not a filter directive: write a level, or `target=level`",
self.0
)
}
}
impl std::error::Error for BadDirective {}
fn target_matches(target: &str, prefix: &str) -> bool {
target.starts_with(prefix)
&& (target.len() == prefix.len() || target[prefix.len()..].starts_with("::"))
}
const fn level_directive(level: LevelFilter) -> &'static str {
match level {
LevelFilter::Off => "off",
LevelFilter::Error => "error",
LevelFilter::Warn => "warn",
LevelFilter::Info => "info",
LevelFilter::Debug => "debug",
LevelFilter::Trace => "trace",
}
}
fn parse_level(text: &str) -> Option<LevelFilter> {
match text.to_ascii_lowercase().as_str() {
"off" | "none" => Some(LevelFilter::Off),
"error" => Some(LevelFilter::Error),
"warn" | "warning" => Some(LevelFilter::Warn),
"info" => Some(LevelFilter::Info),
"debug" => Some(LevelFilter::Debug),
"trace" => Some(LevelFilter::Trace),
_ => None,
}
}
#[derive(Debug)]
struct StderrLogger {
start: Instant,
file: Mutex<Option<FileSink>>,
}
impl StderrLogger {
fn new() -> Self {
Self {
start: Instant::now(),
file: Mutex::new(None),
}
}
fn attach(&self, dir: &Path, stem: &str, kept: usize, max_bytes: u64) -> io::Result<PathBuf> {
let mut slot = self.file.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(open) = slot.as_ref() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("this run already logs to {}", open.path().display()),
));
}
let mut sink = FileSink::open(dir, stem, kept, max_bytes)?;
sink.write_line(&line(
self.start.elapsed(),
Level::Info,
"crcbl_core::log",
format_args!("run started {}", start_banner()),
));
let path = sink.path().to_owned();
*slot = Some(sink);
Ok(path)
}
fn permits(&self, metadata: &Metadata<'_>) -> bool {
read_filter().permits(metadata.level(), metadata.target())
}
fn emit(&self, level: Level, target: &str, args: fmt::Arguments<'_>) {
let elapsed = self.start.elapsed();
push_captured(level, target, args);
console::push(level, target, elapsed, args);
if !self.permits(&Metadata::builder().level(level).target(target).build()) {
return;
}
let line = line(elapsed, level, target, args);
let _ = std::io::stderr().lock().write_all(line.as_bytes());
if let Some(file) = self
.file
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_mut()
{
file.write_line(&line);
}
}
}
impl Log for StderrLogger {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
self.permits(metadata) || capturing()
}
fn log(&self, record: &Record<'_>) {
self.emit(record.level(), record.target(), *record.args());
}
fn flush(&self) {
let _ = std::io::stderr().lock().flush();
}
}
#[doc(hidden)]
pub fn __emit(level: Level, target: &str, args: fmt::Arguments<'_>) {
::log::logger().log(
&Record::builder()
.level(level)
.target(target)
.args(args)
.build(),
);
}
#[doc(hidden)]
#[must_use]
pub fn __enabled(level: Level, target: &str) -> bool {
level <= ::log::max_level()
&& ::log::logger().enabled(&Metadata::builder().level(level).target(target).build())
}
fn line(elapsed: Duration, level: Level, target: &str, args: fmt::Arguments<'_>) -> String {
format!(
"[{elapsed:9.4}s {level:<5} {target}] {args}\n",
elapsed = elapsed.as_secs_f64(),
level = level_name(level),
)
}
const fn level_name(level: Level) -> &'static str {
match level {
Level::Error => "ERROR",
Level::Warn => "WARN",
Level::Info => "INFO",
Level::Debug => "DEBUG",
Level::Trace => "TRACE",
}
}
#[macro_export]
macro_rules! log_at {
($level:expr, $($arg:tt)+) => {{
let level = $level;
let target = ::core::module_path!();
if $crate::log::__enabled(level, target) {
$crate::log::__emit(level, target, ::core::format_args!($($arg)+));
}
}};
}
#[macro_export]
macro_rules! error {
($($arg:tt)+) => { $crate::log_at!($crate::log::Level::Error, $($arg)+) };
}
#[macro_export]
macro_rules! warn {
($($arg:tt)+) => { $crate::log_at!($crate::log::Level::Warn, $($arg)+) };
}
#[macro_export]
macro_rules! info {
($($arg:tt)+) => { $crate::log_at!($crate::log::Level::Info, $($arg)+) };
}
#[macro_export]
macro_rules! debug {
($($arg:tt)+) => { $crate::log_at!($crate::log::Level::Debug, $($arg)+) };
}
#[macro_export]
macro_rules! trace {
($($arg:tt)+) => { $crate::log_at!($crate::log::Level::Trace, $($arg)+) };
}
static LOGGER: OnceLock<StderrLogger> = OnceLock::new();
static INSTALLED: AtomicBool = AtomicBool::new(false);
static SINK_REGISTERED: AtomicBool = AtomicBool::new(false);
static FILTER: RwLock<Filter> = RwLock::new(Filter::INITIAL);
fn read_filter() -> RwLockReadGuard<'static, Filter> {
FILTER.read().unwrap_or_else(PoisonError::into_inner)
}
fn store_filter(filter: Filter) {
::log::set_max_level(filter.max_level());
*FILTER.write().unwrap_or_else(PoisonError::into_inner) = filter;
}
fn honoured() -> bool {
is_installed() || SINK_REGISTERED.load(Ordering::Relaxed)
}
pub fn init_logging() -> bool {
try_init_logging(Filter::from_env()).is_ok()
}
pub fn try_init_logging(filter: Filter) -> Result<(), SetLoggerError> {
let logger = LOGGER.get_or_init(StderrLogger::new);
::log::set_logger(logger)?;
INSTALLED.store(true, Ordering::Relaxed);
store_filter(filter);
logger.emit(
Level::Info,
"crcbl_core::log",
format_args!("run started {}", start_banner()),
);
Ok(())
}
fn start_banner() -> String {
let Ok(since_epoch) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
return "at an unknown time (the system clock is before 1970)".to_owned();
};
let secs = since_epoch.as_secs();
let (year, month, day) = civil_from_days((secs / 86_400) as i64);
let time = secs % 86_400;
format!(
"{year:04}-{month:02}-{day:02} {h:02}:{m:02}:{s:02} UTC",
h = time / 3600,
m = (time % 3600) / 60,
s = time % 60,
)
}
const fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let day_of_era = z - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let shifted_month = (5 * day_of_year + 2) / 153;
let day = (day_of_year - (153 * shifted_month + 2) / 5 + 1) as u32;
let month = if shifted_month < 10 {
shifted_month + 3
} else {
shifted_month - 9
} as u32;
(if month <= 2 { year + 1 } else { year }, month, day)
}
pub fn attach_file(dir: &Path, stem: &str) -> io::Result<PathBuf> {
let logger = installed_logger()?;
let path = logger.attach(dir, stem, LOG_FILES_KEPT, LOG_FILE_MAX_BYTES)?;
if let Err(error) = panic_hook::install(logger) {
warn!(
"log: panics will not be written to {} ({error})",
path.display()
);
}
Ok(path)
}
pub fn attach_file_without_panic_hook(dir: &Path, stem: &str) -> io::Result<PathBuf> {
installed_logger()?.attach(dir, stem, LOG_FILES_KEPT, LOG_FILE_MAX_BYTES)
}
fn installed_logger() -> io::Result<&'static StderrLogger> {
match LOGGER.get() {
Some(logger) if is_installed() => Ok(logger),
_ => Err(io::Error::other(
"the engine's logger is not the process's, so there is no log to copy to a file",
)),
}
}
#[must_use]
pub fn file_path() -> Option<PathBuf> {
let logger = LOGGER.get()?;
let slot = logger.file.lock().unwrap_or_else(PoisonError::into_inner);
slot.as_ref().map(|sink| sink.path().to_owned())
}
#[must_use]
pub fn file_requested() -> bool {
file_requested_by(env::var(FILE_ENV_VAR).ok().as_deref())
}
fn file_requested_by(value: Option<&str>) -> bool {
value.and_then(crate::trace::parse_enabled) == Some(true)
}
#[must_use]
pub fn is_installed() -> bool {
INSTALLED.load(Ordering::Relaxed)
}
pub fn register_sink(filter: Filter) {
SINK_REGISTERED.store(true, Ordering::Relaxed);
store_filter(filter);
}
#[must_use]
pub fn sink_permits(level: Level, target: &str) -> bool {
if !SINK_REGISTERED.load(Ordering::Relaxed) {
return level <= ::log::max_level();
}
read_filter().permits(level, target)
}
#[must_use]
pub fn filter() -> Option<Filter> {
honoured().then(|| read_filter().clone())
}
pub fn set_filter(filter: Filter) -> bool {
if !honoured() {
return false;
}
store_filter(filter);
true
}
crcbl_console::concommand! {
pub fn log(cx, args) {
if args.is_empty() {
let Some(current) = filter() else {
return Err(Fault::new(
"the process's logger honours no engine filter, so there is none to read",
));
};
cx.print(format!("log {current}"));
return Ok(());
}
let directives = args.join(",");
let wanted = Filter::try_parse(&directives).map_err(|bad| Fault::new(bad.to_string()))?;
let installed = wanted.to_string();
if !set_filter(wanted) {
return Err(Fault::new(
"the process's logger honours no engine filter, so it was left alone",
));
}
cx.print(format!("log {installed}"));
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CapturedRecord {
pub level: Level,
pub target: String,
pub message: String,
}
const PROBE_TARGET: &str = "crcbl_core::log::capture";
static CAPTURE_SETUP: Mutex<()> = Mutex::new(());
thread_local! {
static CAPTURED: RefCell<Option<Vec<CapturedRecord>>> = const { RefCell::new(None) };
}
fn capturing() -> bool {
CAPTURED.with(|slot| slot.borrow().is_some())
}
fn push_captured(level: Level, target: &str, args: fmt::Arguments<'_>) {
if !capturing() {
return;
}
let captured = CapturedRecord {
level,
target: target.to_owned(),
message: args.to_string(),
};
CAPTURED.with(|slot| {
if let Some(buffer) = slot.borrow_mut().as_mut() {
buffer.push(captured);
}
});
}
#[derive(Debug)]
pub struct Capture {
_not_send: PhantomData<*const ()>,
}
impl Capture {
#[must_use]
pub fn records(&self) -> Vec<CapturedRecord> {
CAPTURED.with(|slot| slot.borrow().clone().unwrap_or_default())
}
}
impl Drop for Capture {
fn drop(&mut self) {
CAPTURED.with(|slot| *slot.borrow_mut() = None);
}
}
#[must_use]
pub fn capture() -> Capture {
let _setup = CAPTURE_SETUP.lock().unwrap_or_else(PoisonError::into_inner);
let _ = init_logging();
CAPTURED.with(|slot| {
let mut slot = slot.borrow_mut();
assert!(
slot.is_none(),
"this thread is already capturing log records"
);
*slot = Some(Vec::new());
});
if ::log::max_level() < LevelFilter::Trace {
::log::set_max_level(LevelFilter::Trace);
}
let capture = Capture {
_not_send: PhantomData,
};
::log::trace!(target: PROBE_TARGET, "capture probe");
let wired = CAPTURED.with(|slot| {
let mut slot = slot.borrow_mut();
let buffer = slot.as_mut().expect("this thread's capture was just set");
let wired = buffer.iter().any(|record| record.target == PROBE_TARGET);
buffer.clear();
wired
});
assert!(
wired,
"log capture is not wired up: the process logger is not crcbl-core's, \
or this level was compiled out"
);
capture
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_level_sets_the_default() {
let filter = Filter::parse("debug");
assert_eq!(filter.level_for("anything"), LevelFilter::Debug);
assert_eq!(filter.max_level(), LevelFilter::Debug);
}
#[test]
fn empty_directives_fall_back_to_the_default_filter() {
assert_eq!(Filter::parse("").level_for("crcbl_core"), LevelFilter::Info);
assert_eq!(Filter::default().level_for("crcbl_core"), LevelFilter::Info);
}
#[test]
fn per_target_overrides_apply_to_submodules_only() {
let filter = Filter::parse("warn,crcbl_render=debug");
assert_eq!(filter.level_for("crcbl_core"), LevelFilter::Warn);
assert_eq!(filter.level_for("crcbl_render"), LevelFilter::Debug);
assert_eq!(filter.level_for("crcbl_render::graph"), LevelFilter::Debug);
assert_eq!(filter.level_for("crcbl_renderer"), LevelFilter::Warn);
assert_eq!(filter.max_level(), LevelFilter::Debug);
}
#[test]
fn the_longest_matching_directive_wins() {
let filter = Filter::parse("off,crcbl_render=warn,crcbl_render::graph=trace");
assert_eq!(filter.level_for("crcbl_vk"), LevelFilter::Off);
assert_eq!(filter.level_for("crcbl_render"), LevelFilter::Warn);
assert_eq!(filter.level_for("crcbl_render::graph"), LevelFilter::Trace);
assert_eq!(
filter.level_for("crcbl_render::graph::pass"),
LevelFilter::Trace
);
assert_eq!(filter.max_level(), LevelFilter::Trace);
}
#[test]
fn overrides_without_a_bare_level_keep_the_default() {
let filter = Filter::parse("crcbl_vk=trace");
assert_eq!(filter.level_for("crcbl_core"), LevelFilter::Info);
assert_eq!(filter.level_for("crcbl_vk"), LevelFilter::Trace);
}
#[test]
fn nonsense_directives_are_ignored_not_fatal() {
let filter = Filter::parse("info,, ,crcbl_vk=louder,=warn,bogus");
assert_eq!(filter.level_for("crcbl_core"), LevelFilter::Info);
assert_eq!(filter.level_for("crcbl_vk"), LevelFilter::Info);
assert_eq!(
filter.level_for(""),
LevelFilter::Warn,
"an empty target matches everything"
);
}
#[test]
fn level_aliases_and_case_are_accepted() {
let filter = Filter::parse("WARNING,crcbl_net=NONE,crcbl_ui=Error");
assert_eq!(filter.level_for("x"), LevelFilter::Warn);
assert_eq!(filter.level_for("crcbl_net"), LevelFilter::Off);
assert_eq!(filter.level_for("crcbl_ui"), LevelFilter::Error);
}
#[test]
fn the_filter_round_trips_through_its_own_display() {
for directives in [
"info",
"off",
"warn,crcbl_render=debug",
"off,crcbl_render=warn,crcbl_render::graph=trace",
"crcbl_vk=trace",
] {
let filter = Filter::parse(directives);
let written = filter.to_string();
let reparsed = Filter::parse(&written);
assert_eq!(reparsed.to_string(), written, "{directives}");
for target in [
"crcbl_core",
"crcbl_render",
"crcbl_render::graph",
"crcbl_vk",
] {
assert_eq!(
reparsed.level_for(target),
filter.level_for(target),
"{directives} at {target}",
);
}
}
}
#[test]
fn display_writes_the_default_then_the_overrides_in_match_order() {
let filter = Filter::parse("crcbl_render::graph=trace,warn,crcbl_render=debug");
assert_eq!(
filter.to_string(),
"warn,crcbl_render::graph=trace,crcbl_render=debug",
);
}
#[test]
fn try_parse_refuses_the_directive_parse_would_skip() {
let refused = Filter::try_parse("info,crcbl_vk=louder").expect_err("`louder` is no level");
assert_eq!(refused.directive(), "crcbl_vk=louder");
assert!(refused.to_string().contains("crcbl_vk=louder"), "{refused}");
assert_eq!(
Filter::parse("info,crcbl_vk=louder").level_for("crcbl_vk"),
LevelFilter::Info,
"the lenient form still skips it",
);
assert_eq!(
Filter::try_parse("bogus")
.expect_err("a bare word is no level")
.directive(),
"bogus",
);
assert!(
Filter::try_parse("warn,, ,crcbl_vk=trace").is_ok(),
"empties are not directives"
);
}
static FILTER_ORDER: Mutex<()> = Mutex::new(());
fn with_filter<R>(directives: &str, body: impl FnOnce() -> R) -> R {
let _order = FILTER_ORDER.lock().unwrap_or_else(PoisonError::into_inner);
let previous = std::mem::replace(
&mut *FILTER.write().unwrap_or_else(PoisonError::into_inner),
Filter::parse(directives),
);
let result = body();
*FILTER.write().unwrap_or_else(PoisonError::into_inner) = previous;
result
}
#[test]
fn the_const_initial_filter_is_the_default_one_parsed() {
assert_eq!(
Filter::INITIAL.to_string(),
Filter::parse(DEFAULT_FILTER).to_string(),
);
}
#[test]
fn the_ring_holds_records_the_filter_refused() {
let quiet = "crcbl_core::log::tests::refused";
let loud = "crcbl_core::log::tests::printed";
let logger = StderrLogger::new();
with_filter(&format!("info,{quiet}=off"), || {
assert!(
!logger.permits(
&Metadata::builder()
.level(Level::Error)
.target(quiet)
.build()
),
"the directive silences that target, so none of its records reach stderr \
— filter {filter:?}, max level {max_level}, installed {installed}",
filter = *read_filter(),
max_level = ::log::max_level(),
installed = is_installed(),
);
assert!(
logger.permits(&Metadata::builder().level(Level::Info).target(loud).build()),
"the default level admits the other target",
);
logger.emit(Level::Error, quiet, format_args!("under the filter"));
logger.emit(Level::Info, loud, format_args!("over the filter"));
});
let mine: Vec<console::Record> = console::snapshot()
.into_iter()
.filter(|record| record.target == quiet || record.target == loud)
.collect();
assert_eq!(
mine.iter()
.map(|record| (record.target.as_str(), record.message.as_str()))
.collect::<Vec<_>>(),
[(quiet, "under the filter"), (loud, "over the filter")],
"both records are in the ring, whatever stderr got",
);
assert_eq!(mine[0].level, Level::Error);
}
#[test]
fn logger_respects_the_filter_without_being_installed() {
let logger = StderrLogger::new();
with_filter("warn,noisy=trace", || {
assert!(
logger.enabled(
&Metadata::builder()
.level(Level::Error)
.target("any")
.build()
)
);
let meta = Metadata::builder().level(Level::Info).target("any").build();
assert!(
!logger.enabled(&meta),
"an uninstalled logger admits nothing at the default level \
— filter {filter:?}, max level {max_level}, permits {permits}, \
capturing {capturing}, installed {installed}, thread {thread:?}",
filter = *read_filter(),
max_level = ::log::max_level(),
permits = logger.permits(&meta),
capturing = capturing(),
installed = is_installed(),
thread = std::thread::current().name(),
);
assert!(
logger.enabled(
&Metadata::builder()
.level(Level::Trace)
.target("noisy")
.build()
)
);
logger.log(
&Record::builder()
.args(format_args!("hello"))
.target("noisy")
.build(),
);
});
logger.flush();
}
const HOST_SLOT_CHILD: &str = "CRCBL_LOG_HOST_SLOT_CHILD";
#[test]
#[cfg_attr(miri, ignore = "miri cannot spawn the child process this needs")]
fn a_host_owning_the_slot_means_this_module_installed_nothing() {
if env::var(HOST_SLOT_CHILD).is_ok() {
struct HostLogger;
impl Log for HostLogger {
fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
false
}
fn log(&self, _record: &Record<'_>) {}
fn flush(&self) {}
}
static HOST: HostLogger = HostLogger;
assert!(!is_installed(), "nothing has installed anything yet");
::log::set_logger(&HOST).expect("the host wins the empty slot");
assert!(
try_init_logging(Filter::parse("trace")).is_err(),
"the slot is taken, so this must not claim it"
);
assert!(
!is_installed(),
"log::set_logger rejected this module's logger, so it is not installed, \
whatever LOGGER holds"
);
assert!(!init_logging(), "the convenience form agrees");
assert!(!is_installed());
return;
}
let output = crcbl_spawn::command(
std::env::current_exe().expect("a test binary knows its own path"),
)
.args([
"--exact",
"log::tests::a_host_owning_the_slot_means_this_module_installed_nothing",
])
.env(HOST_SLOT_CHILD, "1")
.output()
.expect("re-running this test binary");
let report = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success(), "the child failed:\n{report}");
assert!(
report.contains("1 passed"),
"the child ran no test — has this one been renamed?\n{report}"
);
}
#[test]
fn installing_the_logger_is_idempotent() {
let _order = FILTER_ORDER.lock().unwrap_or_else(PoisonError::into_inner);
assert!(!is_installed());
assert!(try_init_logging(Filter::parse("trace")).is_ok());
assert!(is_installed());
assert_eq!(
file_path(),
None,
"installing the logger opens no file: the file is opt-in"
);
assert_eq!(::log::max_level(), LevelFilter::Trace);
::log::info!("crcbl-core log smoke test");
::log::logger().flush();
assert!(try_init_logging(Filter::parse("off")).is_err());
assert!(!init_logging());
assert_eq!(
::log::max_level(),
LevelFilter::Trace,
"the first filter stays in force"
);
}
#[test]
fn only_a_yes_asks_for_the_log_file() {
for value in [
None,
Some(""),
Some("0"),
Some("off"),
Some("no"),
Some("yse"),
] {
assert!(!file_requested_by(value), "{value:?}");
}
for value in ["1", "true", " ON ", "yes"] {
assert!(file_requested_by(Some(value)), "{value:?}");
}
}
#[test]
fn a_thread_that_never_asked_captures_nothing() {
assert!(!capturing());
push_captured(
Level::Info,
"crcbl_core",
format_args!("dropped on the floor"),
);
assert!(!capturing());
}
#[test]
fn the_civil_date_matches_known_timestamps() {
for (secs, want) in [
(0_i64, (1970, 1, 1)),
(86_399, (1970, 1, 1)),
(86_400, (1970, 1, 2)),
(951_782_400, (2000, 2, 29)), (1_078_012_800, (2004, 2, 29)),
(1_709_164_800, (2024, 2, 29)),
(1_755_216_000, (2025, 8, 15)),
(4_102_444_800, (2100, 1, 1)), ] {
assert_eq!(
civil_from_days(secs / 86_400),
(want.0, want.1, want.2),
"{secs}"
);
}
}
#[test]
fn days_from_civil_round_trips_every_day_for_four_centuries() {
let mut previous = civil_from_days(0);
for day in 1..=146_097_i64 {
let (year, month, dom) = civil_from_days(day);
assert!((1..=12).contains(&month), "day {day} gave month {month}");
assert!((1..=31).contains(&dom), "day {day} gave day {dom}");
assert!(
(year, month, dom) > previous,
"day {day}: {previous:?} then {:?}",
(year, month, dom)
);
previous = (year, month, dom);
}
}
#[test]
fn the_start_banner_names_a_date_and_a_time() {
let banner = start_banner();
assert!(banner.ends_with(" UTC"), "{banner}");
let (date, rest) = banner.split_once(' ').expect("date then time");
assert_eq!(date.len(), "YYYY-MM-DD".len(), "{banner}");
assert_eq!(date.matches('-').count(), 2, "{banner}");
assert_eq!(rest.split_once(' ').expect("time then zone").0.len(), 8);
}
#[test]
fn level_names_are_padded_consistently() {
for level in [
Level::Error,
Level::Warn,
Level::Info,
Level::Debug,
Level::Trace,
] {
assert!(level_name(level).len() <= 5);
}
}
}