#![allow(unsafe_code)]
use std::sync::Mutex;
use pmpx_plugin::abi::{
PmpxLog, PmpxStr, PMPX_LEVEL_DEBUG, PMPX_LEVEL_ERROR, PMPX_LEVEL_INFO, PMPX_LEVEL_WARN,
};
static LINES: Mutex<Vec<(u32, String)>> = Mutex::new(Vec::new());
static TRACE: PmpxLog = table(PMPX_LEVEL_DEBUG);
static INFO: PmpxLog = table(PMPX_LEVEL_INFO);
static NORMAL: PmpxLog = table(PMPX_LEVEL_WARN);
static QUIET: PmpxLog = table(PMPX_LEVEL_ERROR);
const fn table(max_level: u32) -> PmpxLog {
PmpxLog {
size: std::mem::size_of::<PmpxLog>(),
write: record,
max_level,
}
}
unsafe extern "C" fn record(level: u32, message: PmpxStr) {
let bytes = unsafe { message.as_bytes() }.unwrap_or(&[]);
let text = String::from_utf8_lossy(bytes).into_owned();
if let Ok(mut lines) = LINES.lock() {
lines.push((level, text));
}
}
pub struct Captured;
impl Captured {
pub fn take(&self) -> Vec<String> {
let mut lines = LINES.lock().expect("the fake host's list is not poisoned");
lines.drain(..).map(|(_, text)| text).collect()
}
pub fn take_levels(&self) -> Vec<(u32, String)> {
let mut lines = LINES.lock().expect("the fake host's list is not poisoned");
lines.drain(..).collect()
}
pub fn level(self, level: u32) -> Self {
let table = match level {
PMPX_LEVEL_DEBUG => &TRACE,
PMPX_LEVEL_INFO => &INFO,
PMPX_LEVEL_ERROR => &QUIET,
_ => &NORMAL,
};
unsafe { pmpx_plugin::shell::install_log_table(table) };
self
}
pub fn trace(self) -> Self {
self.level(PMPX_LEVEL_DEBUG)
}
pub fn quiet(self) -> Self {
self.level(PMPX_LEVEL_ERROR)
}
pub fn normal(self) -> Self {
self.level(PMPX_LEVEL_WARN)
}
pub fn info(self) -> Self {
self.level(PMPX_LEVEL_INFO)
}
}
pub fn capture() -> Captured {
if let Ok(mut lines) = LINES.lock() {
lines.clear();
}
Captured.level(PMPX_LEVEL_WARN)
}
#[cfg(test)]
mod tests {
use super::*;
use pmpx_plugin::{CommandSpec, Context, Family, PackageManager, PluginError, Verb};
static SERIAL: Mutex<()> = Mutex::new(());
struct Talker;
impl PackageManager for Talker {
fn name(&self) -> &str {
"talker"
}
fn family(&self) -> Family {
Family::NODE
}
fn command(
&self,
_ctx: &Context,
_verb: Verb,
_args: &[std::ffi::OsString],
) -> Result<CommandSpec, PluginError> {
pmpx_plugin::debug!("choosing a version");
pmpx_plugin::warn!("no lockfile, guessing");
pmpx_plugin::error!("this will not work");
Ok(CommandSpec::new("tool"))
}
}
#[test]
fn a_trace_captures_every_level() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let host = capture().trace();
let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
let lines = host.take_levels();
for level in [PMPX_LEVEL_DEBUG, PMPX_LEVEL_WARN, PMPX_LEVEL_ERROR] {
assert!(
lines.iter().any(|(got, _)| *got == level),
"level {level} is missing from {lines:?}"
);
}
assert!(
lines.iter().any(|(_, text)| text == "choosing a version"),
"{lines:?}"
);
}
#[test]
fn a_normal_capture_drops_notes_and_detail() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let host = capture().normal();
let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
let lines = host.take_levels();
assert!(
lines
.iter()
.any(|(_, text)| text == "no lockfile, guessing"),
"a warning is shown by a normal run: {lines:?}"
);
assert!(
!lines.iter().any(|(level, _)| *level == PMPX_LEVEL_DEBUG),
"detail is not even formatted: {lines:?}"
);
assert!(host.take().is_empty(), "the list is drained by taking it");
}
#[test]
fn a_quiet_capture_keeps_only_errors() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let host = capture().quiet();
let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
let lines = host.take_levels();
assert!(
lines.iter().any(|(_, text)| text == "this will not work"),
"{lines:?}"
);
assert!(
lines.iter().all(|(level, _)| *level == PMPX_LEVEL_ERROR),
"a quiet host keeps only failures: {lines:?}"
);
}
}