1#![allow(unsafe_code)] use std::sync::Mutex;
12
13use pmpx_plugin::abi::{
14 PmpxLog, PmpxStr, PMPX_LEVEL_DEBUG, PMPX_LEVEL_ERROR, PMPX_LEVEL_INFO, PMPX_LEVEL_WARN,
15};
16
17static LINES: Mutex<Vec<(u32, String)>> = Mutex::new(Vec::new());
19
20static TRACE: PmpxLog = table(PMPX_LEVEL_DEBUG);
26static INFO: PmpxLog = table(PMPX_LEVEL_INFO);
27static NORMAL: PmpxLog = table(PMPX_LEVEL_WARN);
28static QUIET: PmpxLog = table(PMPX_LEVEL_ERROR);
29
30const fn table(max_level: u32) -> PmpxLog {
32 PmpxLog {
33 size: std::mem::size_of::<PmpxLog>(),
34 write: record,
35 max_level,
36 }
37}
38
39unsafe extern "C" fn record(level: u32, message: PmpxStr) {
44 let bytes = unsafe { message.as_bytes() }.unwrap_or(&[]);
46 let text = String::from_utf8_lossy(bytes).into_owned();
47
48 if let Ok(mut lines) = LINES.lock() {
49 lines.push((level, text));
50 }
51}
52
53pub struct Captured;
55
56impl Captured {
57 pub fn take(&self) -> Vec<String> {
61 let mut lines = LINES.lock().expect("the fake host's list is not poisoned");
62 lines.drain(..).map(|(_, text)| text).collect()
63 }
64
65 pub fn take_levels(&self) -> Vec<(u32, String)> {
67 let mut lines = LINES.lock().expect("the fake host's list is not poisoned");
68 lines.drain(..).collect()
69 }
70
71 pub fn level(self, level: u32) -> Self {
73 let table = match level {
74 PMPX_LEVEL_DEBUG => &TRACE,
75 PMPX_LEVEL_INFO => &INFO,
76 PMPX_LEVEL_ERROR => &QUIET,
77 _ => &NORMAL,
78 };
79 unsafe { pmpx_plugin::shell::install_log_table(table) };
81 self
82 }
83
84 pub fn trace(self) -> Self {
86 self.level(PMPX_LEVEL_DEBUG)
87 }
88
89 pub fn quiet(self) -> Self {
91 self.level(PMPX_LEVEL_ERROR)
92 }
93
94 pub fn normal(self) -> Self {
96 self.level(PMPX_LEVEL_WARN)
97 }
98
99 pub fn info(self) -> Self {
101 self.level(PMPX_LEVEL_INFO)
102 }
103}
104
105pub fn capture() -> Captured {
110 if let Ok(mut lines) = LINES.lock() {
111 lines.clear();
112 }
113
114 Captured.level(PMPX_LEVEL_WARN)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use pmpx_plugin::{CommandSpec, Context, Family, PackageManager, PluginError, Verb};
121
122 static SERIAL: Mutex<()> = Mutex::new(());
126
127 struct Talker;
129
130 impl PackageManager for Talker {
131 fn name(&self) -> &str {
132 "talker"
133 }
134
135 fn family(&self) -> Family {
136 Family::NODE
137 }
138
139 fn command(
140 &self,
141 _ctx: &Context,
142 _verb: Verb,
143 _args: &[std::ffi::OsString],
144 ) -> Result<CommandSpec, PluginError> {
145 pmpx_plugin::debug!("choosing a version");
146 pmpx_plugin::warn!("no lockfile, guessing");
147 pmpx_plugin::error!("this will not work");
148 Ok(CommandSpec::new("tool"))
149 }
150 }
151
152 #[test]
154 fn a_trace_captures_every_level() {
155 let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
156 let host = capture().trace();
157
158 let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
159
160 let lines = host.take_levels();
161 for level in [PMPX_LEVEL_DEBUG, PMPX_LEVEL_WARN, PMPX_LEVEL_ERROR] {
162 assert!(
163 lines.iter().any(|(got, _)| *got == level),
164 "level {level} is missing from {lines:?}"
165 );
166 }
167 assert!(
168 lines.iter().any(|(_, text)| text == "choosing a version"),
169 "{lines:?}"
170 );
171 }
172
173 #[test]
176 fn a_normal_capture_drops_notes_and_detail() {
177 let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
178 let host = capture().normal();
179
180 let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
181
182 let lines = host.take_levels();
183 assert!(
184 lines
185 .iter()
186 .any(|(_, text)| text == "no lockfile, guessing"),
187 "a warning is shown by a normal run: {lines:?}"
188 );
189 assert!(
190 !lines.iter().any(|(level, _)| *level == PMPX_LEVEL_DEBUG),
191 "detail is not even formatted: {lines:?}"
192 );
193 assert!(host.take().is_empty(), "the list is drained by taking it");
194 }
195
196 #[test]
198 fn a_quiet_capture_keeps_only_errors() {
199 let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
200 let host = capture().quiet();
201
202 let _ = Talker.command(&crate::context().build(), Verb::Install, &[]);
203
204 let lines = host.take_levels();
205 assert!(
206 lines.iter().any(|(_, text)| text == "this will not work"),
207 "{lines:?}"
208 );
209 assert!(
210 lines.iter().all(|(level, _)| *level == PMPX_LEVEL_ERROR),
211 "a quiet host keeps only failures: {lines:?}"
212 );
213 }
214}