use std::collections::HashMap;
use std::io::Write;
use std::sync::{Arc, Mutex};
use bitflags::bitflags;
use crate::exception::{AsynException, ExceptionEvent, ExceptionManager};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TraceMask: u32 {
const ERROR = 0x0001;
const IO_DEVICE = 0x0002;
const IO_FILTER = 0x0004;
const IO_DRIVER = 0x0008;
const FLOW = 0x0010;
const WARNING = 0x0020;
}
}
impl TraceMask {
pub fn from_symbolic(s: &str) -> Result<TraceMask, String> {
let mut mask = TraceMask::empty();
for raw in split_mask_tokens(s) {
let tok = raw.trim();
if tok.is_empty() {
continue;
}
if let Some(n) = parse_numeric(tok) {
mask |= TraceMask::from_bits_truncate(n);
continue;
}
let normalized = strip_c_prefixes(tok, &["TRACE_", "TRACEIO_"]);
let bit = match normalized.as_str() {
"ERROR" => TraceMask::ERROR,
"DEVICE" => TraceMask::IO_DEVICE,
"FILTER" => TraceMask::IO_FILTER,
"DRIVER" => TraceMask::IO_DRIVER,
"FLOW" => TraceMask::FLOW,
"WARNING" => TraceMask::WARNING,
_ => {
return Err(format!("unknown trace mask token: '{tok}'"));
}
};
mask |= bit;
}
Ok(mask)
}
}
impl TraceIoMask {
pub fn from_symbolic(s: &str) -> Result<TraceIoMask, String> {
let mut mask = TraceIoMask::empty();
for raw in split_mask_tokens(s) {
let tok = raw.trim();
if tok.is_empty() {
continue;
}
if let Some(n) = parse_numeric(tok) {
mask |= TraceIoMask::from_bits_truncate(n);
continue;
}
let normalized = strip_c_prefixes(tok, &["TRACEIO_"]);
let bit = match normalized.as_str() {
"NODATA" => TraceIoMask::empty(),
"ASCII" => TraceIoMask::ASCII,
"ESCAPE" => TraceIoMask::ESCAPE,
"HEX" => TraceIoMask::HEX,
_ => return Err(format!("unknown trace I/O mask token: '{tok}'")),
};
mask |= bit;
}
Ok(mask)
}
}
impl TraceInfoMask {
pub fn from_symbolic(s: &str) -> Result<TraceInfoMask, String> {
let mut mask = TraceInfoMask::empty();
for raw in split_mask_tokens(s) {
let tok = raw.trim();
if tok.is_empty() {
continue;
}
if let Some(n) = parse_numeric(tok) {
mask |= TraceInfoMask::from_bits_truncate(n);
continue;
}
let normalized = strip_c_prefixes(tok, &["TRACEINFO_"]);
let bit = match normalized.as_str() {
"TIME" => TraceInfoMask::TIME,
"PORT" => TraceInfoMask::PORT,
"SOURCE" => TraceInfoMask::SOURCE,
"THREAD" => TraceInfoMask::THREAD,
_ => return Err(format!("unknown trace info mask token: '{tok}'")),
};
mask |= bit;
}
Ok(mask)
}
}
fn split_mask_tokens(s: &str) -> impl Iterator<Item = &str> {
s.split(['|', '+'])
}
fn strip_c_prefixes(tok: &str, category_prefixes: &[&str]) -> String {
let upper = tok.to_ascii_uppercase();
let stripped = upper.strip_prefix("ASYN_").unwrap_or(&upper);
for p in category_prefixes {
if let Some(rest) = stripped.strip_prefix(p) {
return rest.to_string();
}
}
stripped.to_string()
}
fn parse_numeric(tok: &str) -> Option<u32> {
if let Some(rest) = tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")) {
u32::from_str_radix(rest, 16).ok()
} else if let Some(rest) = tok.strip_prefix("0o").or_else(|| tok.strip_prefix("0O")) {
u32::from_str_radix(rest, 8).ok()
} else if let Some(rest) = tok.strip_prefix('0').filter(|s| !s.is_empty()) {
u32::from_str_radix(rest, 8)
.ok()
.or_else(|| tok.parse::<u32>().ok())
} else {
tok.parse::<u32>().ok()
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TraceIoMask: u32 {
const ASCII = 0x0001;
const ESCAPE = 0x0002;
const HEX = 0x0004;
}
}
impl TraceIoMask {
pub const NODATA: Self = Self::empty();
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TraceInfoMask: u32 {
const TIME = 0x0001;
const PORT = 0x0002;
const SOURCE = 0x0004;
const THREAD = 0x0008;
}
}
pub enum TraceFile {
Stderr,
Stdout,
Errlog,
File(Arc<Mutex<std::fs::File>>),
}
impl TraceFile {
pub fn id(&self) -> usize {
match self {
TraceFile::Errlog => 0,
TraceFile::Stdout => 1,
TraceFile::Stderr => 2,
TraceFile::File(f) => Arc::as_ptr(f) as usize,
}
}
pub fn write_line(&self, line: &str) {
self.write_bytes(line.as_bytes());
}
pub fn write_bytes(&self, line: &[u8]) {
match self {
TraceFile::Stderr | TraceFile::Errlog => {
let _ = std::io::stderr().write_all(line);
}
TraceFile::Stdout => {
let _ = std::io::stdout().write_all(line);
}
TraceFile::File(f) => {
if let Ok(mut f) = f.lock() {
let _ = f.write_all(line);
}
}
}
}
}
impl Default for TraceFile {
fn default() -> Self {
TraceFile::Stderr
}
}
#[derive(Clone, Copy, Debug)]
pub struct TraceSnapshot {
pub trace_mask: TraceMask,
pub io_mask: TraceIoMask,
pub info_mask: TraceInfoMask,
pub io_truncate_size: usize,
pub file_id: usize,
}
const DEFAULT_TRACE_BUFFER_SIZE: usize = 80;
pub struct TraceConfig {
pub trace_mask: TraceMask,
pub trace_io_mask: TraceIoMask,
pub trace_info_mask: TraceInfoMask,
pub io_truncate_size: usize,
pub trace_buffer_size: usize,
pub file: TraceFile,
}
impl Default for TraceConfig {
fn default() -> Self {
Self {
trace_mask: TraceMask::ERROR,
trace_io_mask: TraceIoMask::NODATA,
trace_info_mask: TraceInfoMask::TIME,
io_truncate_size: 80,
trace_buffer_size: DEFAULT_TRACE_BUFFER_SIZE,
file: TraceFile::default(),
}
}
}
struct PortTrace {
port: TraceConfig,
devices: HashMap<i32, TraceConfig>,
multi_device: bool,
}
impl PortTrace {
fn new(multi_device: bool) -> Self {
Self {
port: TraceConfig::default(),
devices: HashMap::new(),
multi_device,
}
}
}
fn device_slot(multi_device: bool, addr: Option<i32>) -> Option<i32> {
crate::port::dp_common_key(multi_device, addr.unwrap_or(-1))
}
pub struct TraceManager {
global_config: Mutex<TraceConfig>,
ports: Mutex<HashMap<String, PortTrace>>,
exception_sink: Mutex<Option<Arc<ExceptionManager>>>,
}
impl TraceManager {
pub fn new() -> Self {
Self {
global_config: Mutex::new(TraceConfig::default()),
ports: Mutex::new(HashMap::new()),
exception_sink: Mutex::new(None),
}
}
pub fn register_port(&self, port: &str, multi_device: bool) {
if let Ok(mut ports) = self.ports.lock() {
ports
.entry(port.to_string())
.or_insert_with(|| PortTrace::new(multi_device))
.multi_device = multi_device;
}
}
pub fn set_exception_sink(&self, sink: Arc<ExceptionManager>) {
if let Ok(mut slot) = self.exception_sink.lock() {
*slot = Some(sink);
}
}
pub fn exception_manager(&self) -> Option<Arc<ExceptionManager>> {
self.exception_sink.lock().ok().and_then(|g| g.clone())
}
fn announce(&self, port: Option<&str>, exception: AsynException) {
let sink = match self.exception_sink.lock() {
Ok(g) => g.clone(),
Err(_) => return,
};
if let Some(sink) = sink {
sink.announce(&ExceptionEvent {
port_name: port.unwrap_or("").to_string(),
exception,
addr: -1,
});
}
}
pub fn is_enabled(&self, port: &str, mask: TraceMask) -> bool {
debug_assert!(
mask.bits().is_power_of_two(),
"is_enabled expects a single trace level, got {:?}",
mask
);
self.with_dp_common(port, None, |cfg| cfg.trace_mask.intersects(mask))
.unwrap_or(false)
}
pub fn is_enabled_device(&self, port: &str, addr: i32, mask: TraceMask) -> bool {
self.with_dp_common(port, Some(addr), |cfg| cfg.trace_mask.intersects(mask))
.unwrap_or(false)
}
fn with_dp_common<R, F>(&self, port: &str, addr: Option<i32>, f: F) -> Option<R>
where
F: FnOnce(&TraceConfig) -> R,
{
let ports = self.ports.lock().ok()?;
let Some(pt) = ports.get(port) else {
return Some(f(&TraceConfig::default()));
};
match device_slot(pt.multi_device, addr) {
Some(a) => Some(f(pt.devices.get(&a).unwrap_or(&pt.port))),
None => Some(f(&pt.port)),
}
}
fn write_scoped<F>(&self, port: &str, addr: Option<i32>, exception: AsynException, mut apply: F)
where
F: FnMut(&mut TraceConfig),
{
let mut announced: Vec<i32> = Vec::new();
if let Ok(mut ports) = self.ports.lock() {
let pt = ports
.entry(port.to_string())
.or_insert_with(|| PortTrace::new(false));
match device_slot(pt.multi_device, addr) {
Some(a) => {
apply(pt.devices.entry(a).or_insert_with(TraceConfig::default));
announced.push(a);
}
None => {
let mut addrs: Vec<i32> = pt.devices.keys().copied().collect();
addrs.sort_unstable();
for a in &addrs {
if let Some(cfg) = pt.devices.get_mut(a) {
apply(cfg);
}
}
apply(&mut pt.port);
announced.extend(addrs);
announced.push(-1);
}
}
}
let sink = self.exception_sink.lock().ok().and_then(|g| g.clone());
if let Some(sink) = sink {
for a in announced {
sink.announce(&ExceptionEvent {
port_name: port.to_string(),
exception,
addr: a,
});
}
}
}
fn write_one_slot<F>(
&self,
port: &str,
addr: Option<i32>,
exception: Option<AsynException>,
apply: F,
) where
F: FnOnce(&mut TraceConfig),
{
let mut announced_at = None;
if let Ok(mut ports) = self.ports.lock() {
let pt = ports
.entry(port.to_string())
.or_insert_with(|| PortTrace::new(false));
match device_slot(pt.multi_device, addr) {
Some(a) => {
apply(pt.devices.entry(a).or_insert_with(TraceConfig::default));
announced_at = Some(a);
}
None => {
apply(&mut pt.port);
announced_at = Some(-1);
}
}
}
let (Some(exception), Some(a)) = (exception, announced_at) else {
return;
};
let sink = self.exception_sink.lock().ok().and_then(|g| g.clone());
if let Some(sink) = sink {
sink.announce(&ExceptionEvent {
port_name: port.to_string(),
exception,
addr: a,
});
}
}
pub fn set_device_trace_mask(&self, port: &str, addr: i32, mask: TraceMask) {
self.write_scoped(port, Some(addr), AsynException::TraceMask, |cfg| {
cfg.trace_mask = mask
});
}
pub fn output(&self, port: &str, mask: TraceMask, msg: &str) {
self.output_device(port, None, 0, mask, msg);
}
pub fn output_device(
&self,
port: &str,
addr: Option<i32>,
reason: i32,
mask: TraceMask,
msg: &str,
) {
self.with_dp_common(port, addr, |cfg| {
if !cfg.trace_mask.intersects(mask) {
return;
}
let prefix = format_prefix_addr(port, addr, reason, ("", 0), cfg);
let line = format!("{prefix}{msg}\n");
cfg.file.write_line(&line);
});
}
pub fn output_with_source(
&self,
port: &str,
mask: TraceMask,
file: &str,
line: u32,
msg: &str,
) {
self.output_device_with_source(port, None, 0, mask, file, line, msg);
}
#[allow(clippy::too_many_arguments)]
pub fn output_device_with_source(
&self,
port: &str,
addr: Option<i32>,
reason: i32,
mask: TraceMask,
file: &str,
line: u32,
msg: &str,
) {
self.with_dp_common(port, addr, |cfg| {
if !cfg.trace_mask.intersects(mask) {
return;
}
let prefix = format_prefix_addr(port, addr, reason, (file, line), cfg);
let out = format!("{prefix}{msg}\n");
cfg.file.write_line(&out);
});
}
pub fn output_io(
&self,
port: &str,
mask: TraceMask,
data: &[u8],
label: &str,
file: &str,
line: u32,
) {
self.output_device_io(port, None, 0, mask, data, label, file, line);
}
#[allow(clippy::too_many_arguments)]
pub fn output_device_io(
&self,
port: &str,
addr: Option<i32>,
reason: i32,
mask: TraceMask,
data: &[u8],
label: &str,
file: &str,
line: u32,
) {
self.with_dp_common(port, addr, |cfg| {
if !cfg.trace_mask.intersects(mask) {
return;
}
let prefix = format_prefix_addr(port, addr, reason, (file, line), cfg);
let mut out = format!("{prefix}{label}\n").into_bytes();
append_io_data(&mut out, data, cfg);
cfg.file.write_bytes(&out);
});
}
pub fn set_trace_mask(&self, port: Option<&str>, mask: TraceMask) {
match port {
Some(name) => {
self.write_scoped(name, None, AsynException::TraceMask, |cfg| {
cfg.trace_mask = mask
});
}
None => {
if let Ok(mut cfg) = self.global_config.lock() {
cfg.trace_mask = mask;
}
self.announce(None, AsynException::TraceMask);
}
}
}
pub fn set_trace_io_mask(&self, port: Option<&str>, mask: TraceIoMask) {
match port {
Some(name) => {
self.write_scoped(name, None, AsynException::TraceIoMask, |cfg| {
cfg.trace_io_mask = mask
});
}
None => {
if let Ok(mut cfg) = self.global_config.lock() {
cfg.trace_io_mask = mask;
}
self.announce(None, AsynException::TraceIoMask);
}
}
}
pub fn set_device_trace_io_mask(&self, port: &str, addr: i32, mask: TraceIoMask) {
self.write_scoped(port, Some(addr), AsynException::TraceIoMask, |cfg| {
cfg.trace_io_mask = mask
});
}
pub fn set_trace_info_mask(&self, port: Option<&str>, mask: TraceInfoMask) {
match port {
Some(name) => {
self.write_scoped(name, None, AsynException::TraceInfoMask, |cfg| {
cfg.trace_info_mask = mask
});
}
None => {
if let Ok(mut cfg) = self.global_config.lock() {
cfg.trace_info_mask = mask;
}
self.announce(None, AsynException::TraceInfoMask);
}
}
}
pub fn set_device_trace_info_mask(&self, port: &str, addr: i32, mask: TraceInfoMask) {
self.write_scoped(port, Some(addr), AsynException::TraceInfoMask, |cfg| {
cfg.trace_info_mask = mask
});
}
pub fn set_trace_file(&self, port: Option<&str>, file: TraceFile) {
match port {
Some(name) => self.write_one_slot(name, None, Some(AsynException::TraceFile), |cfg| {
cfg.file = file
}),
None => {
if let Ok(mut cfg) = self.global_config.lock() {
cfg.file = file;
}
}
}
}
pub fn set_device_trace_file(&self, port: &str, addr: i32, file: TraceFile) {
self.write_one_slot(port, Some(addr), Some(AsynException::TraceFile), |cfg| {
cfg.file = file
});
}
pub fn set_io_truncate_size(&self, port: Option<&str>, size: usize) {
match port {
Some(name) => self.write_one_slot(
name,
None,
Some(AsynException::TraceIoTruncateSize),
|cfg| {
cfg.io_truncate_size = size;
cfg.trace_buffer_size = cfg.trace_buffer_size.max(size);
},
),
None => {
if let Ok(mut cfg) = self.global_config.lock() {
cfg.io_truncate_size = size;
cfg.trace_buffer_size = cfg.trace_buffer_size.max(size);
}
}
}
}
pub fn set_device_io_truncate_size(&self, port: &str, addr: i32, size: usize) {
self.write_one_slot(
port,
Some(addr),
Some(AsynException::TraceIoTruncateSize),
|cfg| {
cfg.io_truncate_size = size;
cfg.trace_buffer_size = cfg.trace_buffer_size.max(size);
},
);
}
pub fn get_trace_mask(&self, port: Option<&str>) -> TraceMask {
match port {
Some(name) => self
.with_dp_common(name, None, |cfg| cfg.trace_mask)
.unwrap_or(TraceMask::ERROR),
None => self
.global_config
.lock()
.map(|c| c.trace_mask)
.unwrap_or(TraceMask::ERROR),
}
}
pub fn get_trace_io_mask(&self, port: Option<&str>) -> TraceIoMask {
match port {
Some(name) => self
.with_dp_common(name, None, |cfg| cfg.trace_io_mask)
.unwrap_or(TraceIoMask::NODATA),
None => self
.global_config
.lock()
.map(|c| c.trace_io_mask)
.unwrap_or(TraceIoMask::NODATA),
}
}
pub fn snapshot(&self, port: &str, addr: Option<i32>) -> TraceSnapshot {
self.with_dp_common(port, addr, |cfg| TraceSnapshot {
trace_mask: cfg.trace_mask,
io_mask: cfg.trace_io_mask,
info_mask: cfg.trace_info_mask,
io_truncate_size: cfg.io_truncate_size,
file_id: cfg.file.id(),
})
.unwrap_or_else(|| {
let cfg = TraceConfig::default();
TraceSnapshot {
trace_mask: cfg.trace_mask,
io_mask: cfg.trace_io_mask,
info_mask: cfg.trace_info_mask,
io_truncate_size: cfg.io_truncate_size,
file_id: cfg.file.id(),
}
})
}
pub fn get_trace_info_mask(&self, port: Option<&str>) -> TraceInfoMask {
match port {
Some(name) => self
.with_dp_common(name, None, |cfg| cfg.trace_info_mask)
.unwrap_or(TraceInfoMask::TIME),
None => self
.global_config
.lock()
.map(|c| c.trace_info_mask)
.unwrap_or(TraceInfoMask::TIME),
}
}
}
impl Default for TraceManager {
fn default() -> Self {
Self::new()
}
}
fn strip_path(file: &str) -> &str {
let after_slash = match file.rfind('/') {
Some(i) => &file[i + 1..],
None => file,
};
#[cfg(windows)]
{
match after_slash.rfind('\\') {
Some(i) => &after_slash[i + 1..],
None => after_slash,
}
}
#[cfg(not(windows))]
after_slash
}
fn format_prefix_addr(
port: &str,
addr: Option<i32>,
reason: i32,
source: (&str, u32),
cfg: &TraceConfig,
) -> String {
let mut out = String::new();
if cfg.trace_info_mask.contains(TraceInfoMask::TIME) {
let now = chrono::Local::now();
out.push_str(&now.format("%Y/%m/%d %H:%M:%S%.3f ").to_string());
}
if cfg.trace_info_mask.contains(TraceInfoMask::PORT) {
let a = addr.unwrap_or(-1);
out.push_str(&format!("[{port},{a},{reason}] "));
}
if cfg.trace_info_mask.contains(TraceInfoMask::SOURCE) {
let (file, line) = source;
out.push_str(&format!("[{}:{}] ", strip_path(file), line));
}
if cfg.trace_info_mask.contains(TraceInfoMask::THREAD) {
let current = std::thread::current();
let name = current.name().unwrap_or("").to_string();
out.push_str(&format!(
"[{name},{},{}] ",
thread_token(),
thread_epics_priority()
));
}
out
}
fn thread_token() -> String {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
std::thread::current().id().hash(&mut h);
format!("{:#x}", h.finish())
}
fn thread_epics_priority() -> u32 {
0
}
fn append_io_data(out: &mut Vec<u8>, data: &[u8], cfg: &TraceConfig) {
use std::fmt::Write as _;
let data = &data[..data.len().min(cfg.io_truncate_size)];
let mask = cfg.trace_io_mask;
if mask.contains(TraceIoMask::ASCII) && !data.is_empty() {
let ascii = match data.iter().position(|&b| b == 0) {
Some(nul) => &data[..nul],
None => data,
};
out.extend_from_slice(ascii);
out.push(b'\n');
}
if mask.contains(TraceIoMask::ESCAPE) && !data.is_empty() {
let escaped = format_escape(data, &cfg.file, cfg.trace_buffer_size);
out.extend_from_slice(escaped.as_bytes());
out.push(b'\n');
}
if mask.contains(TraceIoMask::HEX) && cfg.io_truncate_size > 0 {
let mut hex = String::with_capacity(data.len() * 3 + data.len() / 20 + 2);
for (i, b) in data.iter().enumerate() {
if i % 20 == 0 {
hex.push('\n');
}
let _ = write!(hex, "{b:02x} ");
}
hex.push('\n');
out.extend_from_slice(hex.as_bytes());
}
if mask.is_empty() || cfg.io_truncate_size == 0 {
out.push(b'\n');
}
}
fn format_escape(data: &[u8], dest: &TraceFile, buf_size: usize) -> String {
match dest {
TraceFile::Errlog => crate::escape::escaped_from_raw(data, buf_size),
TraceFile::Stderr | TraceFile::Stdout | TraceFile::File(_) => {
crate::escape::print_escaped(data)
}
}
}
#[macro_export]
macro_rules! asyn_trace {
(Some($mgr:expr), $port:expr, $mask:expr, $($arg:tt)*) => {
if let Some(ref __mgr) = $mgr {
let __mgr: &$crate::trace::TraceManager = __mgr;
if __mgr.is_enabled($port, $mask) {
__mgr.output_with_source($port, $mask, file!(), line!(), &format!($($arg)*));
}
}
};
($mgr:expr, $port:expr, $mask:expr, $($arg:tt)*) => {
if $mgr.is_enabled($port, $mask) {
$mgr.output_with_source($port, $mask, file!(), line!(), &format!($($arg)*));
}
};
}
#[macro_export]
macro_rules! asyn_trace_io {
(Some($mgr:expr), $port:expr, $mask:expr, $data:expr, $($arg:tt)*) => {
if let Some(ref __mgr) = $mgr {
let __mgr: &$crate::trace::TraceManager = __mgr;
if __mgr.is_enabled($port, $mask) {
__mgr.output_io($port, $mask, $data, &format!($($arg)*), file!(), line!());
}
}
};
($mgr:expr, $port:expr, $mask:expr, $data:expr, $($arg:tt)*) => {
if $mgr.is_enabled($port, $mask) {
$mgr.output_io($port, $mask, $data, &format!($($arg)*), file!(), line!());
}
};
}
#[macro_export]
macro_rules! asyn_trace_device {
(Some($mgr:expr), $port:expr, $addr:expr, $mask:expr, $($arg:tt)*) => {
if let Some(ref __mgr) = $mgr {
let __mgr: &$crate::trace::TraceManager = __mgr;
if __mgr.is_enabled_device($port, $addr, $mask) {
__mgr.output_device_with_source(
$port,
Some($addr),
0,
$mask,
file!(),
line!(),
&format!($($arg)*),
);
}
}
};
($mgr:expr, $port:expr, $addr:expr, $mask:expr, $($arg:tt)*) => {
if $mgr.is_enabled_device($port, $addr, $mask) {
$mgr.output_device_with_source(
$port,
Some($addr),
0,
$mask,
file!(),
line!(),
&format!($($arg)*),
);
}
};
}
#[macro_export]
macro_rules! asyn_trace_device_io {
(Some($mgr:expr), $port:expr, $addr:expr, $mask:expr, $data:expr, $($arg:tt)*) => {
if let Some(ref __mgr) = $mgr {
let __mgr: &$crate::trace::TraceManager = __mgr;
if __mgr.is_enabled_device($port, $addr, $mask) {
__mgr.output_device_io(
$port,
Some($addr),
0,
$mask,
$data,
&format!($($arg)*),
file!(),
line!(),
);
}
}
};
($mgr:expr, $port:expr, $addr:expr, $mask:expr, $data:expr, $($arg:tt)*) => {
if $mgr.is_enabled_device($port, $addr, $mask) {
$mgr.output_device_io(
$port,
Some($addr),
0,
$mask,
$data,
&format!($($arg)*),
file!(),
line!(),
);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_mask_error_only() {
let mgr = TraceManager::new();
assert!(mgr.is_enabled("port1", TraceMask::ERROR));
assert!(!mgr.is_enabled("port1", TraceMask::WARNING));
assert!(!mgr.is_enabled("port1", TraceMask::FLOW));
assert!(!mgr.is_enabled("port1", TraceMask::IO_DRIVER));
}
#[test]
fn test_six_bits_match_c_asyn_header() {
assert_eq!(TraceMask::ERROR.bits(), 0x0001);
assert_eq!(TraceMask::IO_DEVICE.bits(), 0x0002);
assert_eq!(TraceMask::IO_FILTER.bits(), 0x0004);
assert_eq!(TraceMask::IO_DRIVER.bits(), 0x0008);
assert_eq!(TraceMask::FLOW.bits(), 0x0010);
assert_eq!(TraceMask::WARNING.bits(), 0x0020);
let all = TraceMask::ERROR
| TraceMask::IO_DEVICE
| TraceMask::IO_FILTER
| TraceMask::IO_DRIVER
| TraceMask::FLOW
| TraceMask::WARNING;
assert_eq!(all.bits(), 0x003F);
}
#[test]
fn test_trace_mask_from_symbolic_basic() {
let m = TraceMask::from_symbolic("ERROR|FLOW|DEVICE").unwrap();
assert_eq!(m, TraceMask::ERROR | TraceMask::FLOW | TraceMask::IO_DEVICE);
}
#[test]
fn test_trace_mask_from_symbolic_long_form_and_plus_separator() {
let m = TraceMask::from_symbolic("ASYN_TRACEIO_DRIVER+ASYN_TRACE_FLOW").unwrap();
assert_eq!(m, TraceMask::IO_DRIVER | TraceMask::FLOW);
}
#[test]
fn test_trace_mask_from_symbolic_case_insensitive_and_aliases() {
let m = TraceMask::from_symbolic("error|asyn_traceio_driver|Warning").unwrap();
assert_eq!(
m,
TraceMask::ERROR | TraceMask::IO_DRIVER | TraceMask::WARNING
);
}
#[test]
fn test_trace_mask_from_symbolic_numeric_mix() {
let m = TraceMask::from_symbolic("ERROR|0x10|0o20").unwrap();
assert_eq!(m, TraceMask::ERROR | TraceMask::FLOW);
}
#[test]
fn test_trace_mask_from_symbolic_unknown_token_errors() {
let err = TraceMask::from_symbolic("ERROR|NOPE").unwrap_err();
assert!(err.contains("NOPE"), "error must name the bad token: {err}");
}
#[test]
fn test_trace_mask_from_symbolic_empty_and_whitespace() {
assert_eq!(TraceMask::from_symbolic("").unwrap(), TraceMask::empty());
assert_eq!(TraceMask::from_symbolic(" ").unwrap(), TraceMask::empty());
assert_eq!(
TraceMask::from_symbolic(" ERROR | | FLOW ").unwrap(),
TraceMask::ERROR | TraceMask::FLOW
);
}
#[test]
fn test_trace_io_mask_and_info_mask_symbolic() {
let io = TraceIoMask::from_symbolic("ESCAPE+HEX").unwrap();
assert_eq!(io, TraceIoMask::ESCAPE | TraceIoMask::HEX);
let io2 = TraceIoMask::from_symbolic("ASYN_TRACEIO_ASCII").unwrap();
assert_eq!(io2, TraceIoMask::ASCII);
let info = TraceInfoMask::from_symbolic("TIME|THREAD").unwrap();
assert_eq!(info, TraceInfoMask::TIME | TraceInfoMask::THREAD);
}
#[test]
fn test_trace_io_nodata_token() {
assert_eq!(
TraceIoMask::from_symbolic("NODATA").unwrap(),
TraceIoMask::empty()
);
assert_eq!(
TraceIoMask::from_symbolic("NODATA+HEX").unwrap(),
TraceIoMask::HEX
);
}
#[test]
fn fresh_port_trace_info_mask_is_time_only() {
assert_eq!(TraceConfig::default().trace_info_mask, TraceInfoMask::TIME);
let mgr = TraceManager::new();
assert_eq!(
mgr.get_trace_info_mask(Some("never-created")),
TraceInfoMask::TIME
);
assert_eq!(mgr.get_trace_info_mask(None), TraceInfoMask::TIME);
assert!(
!TraceConfig::default()
.trace_info_mask
.contains(TraceInfoMask::PORT)
);
}
#[test]
fn test_set_global_mask() {
let mgr = TraceManager::new();
mgr.set_trace_mask(None, TraceMask::ERROR | TraceMask::FLOW);
assert_eq!(mgr.get_trace_mask(None), TraceMask::ERROR | TraceMask::FLOW);
assert!(mgr.is_enabled("any", TraceMask::ERROR));
assert!(!mgr.is_enabled("any", TraceMask::FLOW));
}
#[test]
fn test_port_override_vs_global() {
let mgr = TraceManager::new();
mgr.set_trace_mask(None, TraceMask::ERROR);
mgr.set_trace_mask(Some("myport"), TraceMask::FLOW);
assert!(mgr.is_enabled("myport", TraceMask::FLOW));
assert!(!mgr.is_enabled("myport", TraceMask::ERROR));
assert!(mgr.is_enabled("other", TraceMask::ERROR));
assert!(!mgr.is_enabled("other", TraceMask::FLOW));
}
fn blocks(data: &[u8], mask: TraceIoMask, io_truncate_size: usize) -> Vec<u8> {
let cfg = TraceConfig {
trace_io_mask: mask,
io_truncate_size,
..TraceConfig::default()
};
let mut out = Vec::new();
append_io_data(&mut out, data, &cfg);
out
}
#[test]
fn every_enabled_io_mask_bit_emits_its_own_block_in_c_s_order() {
let data = b"OK\r\n";
assert_eq!(blocks(data, TraceIoMask::ASCII, 80), b"OK\r\n\n");
assert_eq!(blocks(data, TraceIoMask::ESCAPE, 80), b"OK\\r\\n\n");
assert_eq!(blocks(data, TraceIoMask::HEX, 80), b"\n4f 4b 0d 0a \n");
assert_eq!(
blocks(data, TraceIoMask::ASCII | TraceIoMask::HEX, 80),
b"OK\r\n\n\n4f 4b 0d 0a \n"
);
assert_eq!(
blocks(data, TraceIoMask::all(), 80),
b"OK\r\n\nOK\\r\\n\n\n4f 4b 0d 0a \n"
);
}
#[test]
fn the_ascii_block_is_the_raw_bytes_not_a_printable_rendering() {
assert_eq!(blocks(b"hi\r\n", TraceIoMask::ASCII, 80), b"hi\r\n\n");
assert_eq!(blocks(&[0x00, 0x7f, 0x41], TraceIoMask::ASCII, 80), b"\n");
assert_eq!(
blocks(&[0xff, 0xfe], TraceIoMask::ASCII, 80),
&[0xff, 0xfe, b'\n']
);
}
#[test]
fn the_hex_block_wraps_every_twenty_bytes_and_is_newline_wrapped() {
let data: Vec<u8> = (0..25).collect();
let out = String::from_utf8(blocks(&data, TraceIoMask::HEX, 80)).unwrap();
let mut want = String::from("\n");
for b in 0..20u8 {
want.push_str(&format!("{b:02x} "));
}
want.push('\n');
for b in 20..25u8 {
want.push_str(&format!("{b:02x} "));
}
want.push('\n');
assert_eq!(out, want);
assert_eq!(blocks(b"", TraceIoMask::HEX, 80), b"\n");
}
#[test]
fn a_zero_mask_or_a_zero_truncate_size_emits_a_bare_newline() {
assert_eq!(blocks(b"OK", TraceIoMask::NODATA, 80), b"\n");
assert_eq!(blocks(b"OK", TraceIoMask::ASCII, 0), b"\n");
assert_eq!(blocks(b"OK", TraceIoMask::all(), 0), b"\n");
assert_eq!(TraceConfig::default().trace_io_mask, TraceIoMask::NODATA);
assert_eq!(
TraceManager::new().get_trace_io_mask(None),
TraceIoMask::NODATA
);
}
#[test]
fn the_truncate_size_bounds_every_block() {
assert_eq!(blocks(b"hello world", TraceIoMask::ASCII, 4), b"hell\n");
assert_eq!(blocks(b"hello world", TraceIoMask::ESCAPE, 4), b"hell\n");
assert_eq!(
blocks(b"hello world", TraceIoMask::HEX, 4),
b"\n68 65 6c 6c \n"
);
}
#[test]
fn a_first_byte_nul_payload_escapes_to_an_empty_data_line_on_a_file_sink() {
assert_eq!(blocks(b"\0ab", TraceIoMask::ESCAPE, 80), b"\n");
let cfg = TraceConfig {
trace_io_mask: TraceIoMask::ESCAPE,
file: TraceFile::Errlog,
..TraceConfig::default()
};
let mut out = Vec::new();
append_io_data(&mut out, b"\0ab", &cfg);
assert_eq!(out, b"\\0ab\n");
}
#[test]
fn test_format_escape() {
let n = DEFAULT_TRACE_BUFFER_SIZE;
let errlog = TraceFile::Errlog;
assert_eq!(format_escape(b"OK\r\n", &errlog, n), "OK\\r\\n");
assert_eq!(format_escape(b"\t\\", &errlog, n), "\\t\\\\");
assert_eq!(format_escape(&[0x01], &errlog, n), "\\x01");
assert_eq!(format_escape(b"hi", &errlog, n), "hi");
}
#[test]
fn format_escape_is_bounded_by_the_trace_buffer_on_the_errlog_branch_only() {
let crlf: Vec<u8> = b"\r\n".repeat(50);
let out = format_escape(&crlf, &TraceFile::Errlog, DEFAULT_TRACE_BUFFER_SIZE);
assert_eq!(out.len(), DEFAULT_TRACE_BUFFER_SIZE - 1);
assert!(out.ends_with(r"\r\n\r\"), "cut mid-pair, as C does: {out}");
let out = format_escape(&crlf, &TraceFile::Stderr, DEFAULT_TRACE_BUFFER_SIZE);
assert_eq!(out.len(), 200, "epicsStrPrintEscaped writes to a stream");
}
#[test]
fn the_escape_entry_point_is_chosen_by_the_trace_destination() {
let n = DEFAULT_TRACE_BUFFER_SIZE;
let data = b"a\0b";
assert_eq!(format_escape(data, &TraceFile::Errlog, n), r"a\0b");
assert_eq!(format_escape(data, &TraceFile::Stderr, n), r"a\0b");
assert_eq!(format_escape(data, &TraceFile::Stdout, n), r"a\0b");
let dir = tempfile::tempdir().expect("fixture root");
let f = TraceFile::File(Arc::new(Mutex::new(
std::fs::File::create(dir.path().join("asyn_r17_46.txt")).unwrap(),
)));
assert_eq!(format_escape(data, &f, n), r"a\0b");
let _ = std::fs::remove_file(dir.path().join("asyn_r17_46.txt"));
assert!(matches!(TraceConfig::default().file, TraceFile::Stderr));
}
#[test]
fn a_bigger_truncate_size_grows_the_trace_buffer_and_a_smaller_one_does_not_shrink_it() {
let mgr = TraceManager::new();
mgr.set_io_truncate_size(None, 400);
assert_eq!(mgr.global_config.lock().unwrap().trace_buffer_size, 400);
mgr.set_io_truncate_size(None, 8);
assert_eq!(mgr.global_config.lock().unwrap().io_truncate_size, 8);
assert_eq!(mgr.global_config.lock().unwrap().trace_buffer_size, 400);
}
#[test]
fn test_output_to_buffer() {
let mgr = TraceManager::new();
mgr.set_trace_mask(Some("testport"), TraceMask::ERROR | TraceMask::IO_DRIVER);
mgr.set_trace_info_mask(Some("testport"), TraceInfoMask::PORT);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_test.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(
Some("testport"),
TraceFile::File(Arc::new(Mutex::new(file))),
);
mgr.output("testport", TraceMask::ERROR, "something broke");
let contents = std::fs::read_to_string(&temp).unwrap();
assert!(contents.contains("[testport,-1,0] "), "got {contents:?}");
assert!(!contents.contains("ERROR"), "C prints no mask label");
assert!(contents.contains("something broke"));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn test_output_io_to_buffer() {
let mgr = TraceManager::new();
mgr.set_trace_mask(Some("testport"), TraceMask::IO_DRIVER);
mgr.set_trace_info_mask(Some("testport"), TraceInfoMask::PORT);
mgr.set_trace_io_mask(Some("testport"), TraceIoMask::ESCAPE);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_io_test.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(
Some("testport"),
TraceFile::File(Arc::new(Mutex::new(file))),
);
mgr.output_io("testport", TraceMask::IO_DRIVER, b"OK\r\n", "read:", "", 0);
let contents = std::fs::read_to_string(&temp).unwrap();
assert!(contents.contains("[testport,-1,0] "), "got {contents:?}");
assert!(!contents.contains("IO_DRIVER"), "C prints no mask label");
assert!(contents.contains("read:"));
assert!(contents.contains("OK\\r\\n"));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn test_get_masks() {
let mgr = TraceManager::new();
assert_eq!(mgr.get_trace_mask(None), TraceMask::ERROR);
assert_eq!(mgr.get_trace_io_mask(None), TraceIoMask::NODATA);
mgr.set_trace_mask(Some("p1"), TraceMask::FLOW);
assert_eq!(mgr.get_trace_mask(Some("p1")), TraceMask::FLOW);
assert_eq!(mgr.get_trace_mask(None), TraceMask::ERROR);
}
#[test]
fn test_macro_short_circuit() {
let mgr = TraceManager::new();
asyn_trace!(mgr, "port", TraceMask::FLOW, "should not appear");
}
#[test]
fn test_io_truncate_integration() {
let mgr = TraceManager::new();
mgr.set_trace_mask(Some("p"), TraceMask::IO_DRIVER);
mgr.set_trace_info_mask(Some("p"), TraceInfoMask::PORT);
mgr.set_trace_io_mask(Some("p"), TraceIoMask::ASCII);
mgr.set_io_truncate_size(Some("p"), 3);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_trunc_test.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(Some("p"), TraceFile::File(Arc::new(Mutex::new(file))));
mgr.output_io("p", TraceMask::IO_DRIVER, b"hello world", "write:", "", 0);
let contents = std::fs::read_to_string(&temp).unwrap();
assert!(contents.contains("hel"));
assert!(!contents.contains("hello"));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn test_write_line_single_call() {
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_single_write.txt");
let file = std::fs::File::create(&temp).unwrap();
let tf = TraceFile::File(Arc::new(Mutex::new(file)));
tf.write_line("line one\n");
tf.write_line("line two\n");
let contents = std::fs::read_to_string(&temp).unwrap();
assert_eq!(contents, "line one\nline two\n");
let _ = std::fs::remove_file(&temp);
}
#[test]
fn test_set_trace_mask_fires_exception() {
use crate::exception::AsynException;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering as O;
let exc = Arc::new(ExceptionManager::new());
let mgr = TraceManager::new();
mgr.set_exception_sink(exc.clone());
let n = Arc::new(AtomicUsize::new(0));
let captured = Arc::new(Mutex::new(Vec::<AsynException>::new()));
let n2 = n.clone();
let captured2 = captured.clone();
exc.add_callback(move |ev| {
n2.fetch_add(1, O::Relaxed);
captured2.lock().unwrap().push(ev.exception);
});
mgr.set_trace_mask(Some("p"), TraceMask::FLOW);
mgr.set_trace_io_mask(Some("p"), TraceIoMask::HEX);
mgr.set_trace_info_mask(Some("p"), TraceInfoMask::TIME);
let file = TraceFile::Stderr;
mgr.set_trace_file(Some("p"), file);
mgr.set_io_truncate_size(Some("p"), 16);
mgr.set_device_trace_mask("p", 3, TraceMask::ERROR);
assert_eq!(n.load(O::Relaxed), 6);
let exps = captured.lock().unwrap().clone();
assert!(exps.contains(&AsynException::TraceMask));
assert!(exps.contains(&AsynException::TraceIoMask));
assert!(exps.contains(&AsynException::TraceInfoMask));
assert!(exps.contains(&AsynException::TraceFile));
assert!(exps.contains(&AsynException::TraceIoTruncateSize));
}
#[test]
fn test_global_trace_mask_announce() {
use crate::exception::AsynException;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering as O;
let exc = Arc::new(ExceptionManager::new());
let mgr = TraceManager::new();
mgr.set_exception_sink(exc.clone());
let n = Arc::new(AtomicUsize::new(0));
let n2 = n.clone();
exc.add_callback(move |ev| {
if ev.exception == AsynException::TraceMask && ev.port_name.is_empty() {
n2.fetch_add(1, O::Relaxed);
}
});
mgr.set_trace_mask(None, TraceMask::FLOW);
assert_eq!(n.load(O::Relaxed), 1);
}
#[test]
fn test_global_file_and_truncate_do_not_announce() {
use crate::exception::AsynException;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering as O;
let exc = Arc::new(ExceptionManager::new());
let mgr = TraceManager::new();
mgr.set_exception_sink(exc.clone());
let file_hits = Arc::new(AtomicUsize::new(0));
let trunc_hits = Arc::new(AtomicUsize::new(0));
let f2 = file_hits.clone();
let t2 = trunc_hits.clone();
exc.add_callback(move |ev| match ev.exception {
AsynException::TraceFile => {
f2.fetch_add(1, O::Relaxed);
}
AsynException::TraceIoTruncateSize => {
t2.fetch_add(1, O::Relaxed);
}
_ => {}
});
mgr.set_trace_file(None, TraceFile::Stderr);
mgr.set_io_truncate_size(None, 32);
assert_eq!(file_hits.load(O::Relaxed), 0);
assert_eq!(trunc_hits.load(O::Relaxed), 0);
}
fn read_lines(path: &std::path::Path) -> Vec<String> {
std::fs::read_to_string(path)
.unwrap_or_default()
.lines()
.map(|l| l.to_string())
.collect()
}
#[test]
fn output_device_uses_device_config_when_present() {
let mgr = TraceManager::new();
mgr.register_port("dev_p", true);
mgr.set_trace_mask(Some("dev_p"), TraceMask::ERROR);
mgr.set_device_trace_mask("dev_p", 5, TraceMask::ERROR | TraceMask::FLOW);
mgr.set_device_trace_info_mask("dev_p", 5, TraceInfoMask::PORT);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_device_output.txt");
let file = std::fs::File::create(&temp).unwrap();
let tf = TraceFile::File(Arc::new(Mutex::new(file)));
let file2 = std::fs::File::create(&temp).unwrap();
let tf2 = TraceFile::File(Arc::new(Mutex::new(file2)));
mgr.set_device_trace_file("dev_p", 5, tf);
mgr.set_trace_file(Some("dev_p"), tf2);
mgr.output_device("dev_p", Some(5), 0, TraceMask::FLOW, "device-flow");
let lines = read_lines(&temp);
assert!(
lines.iter().any(|l| l.contains("device-flow")),
"device-config output should have been emitted, got {lines:?}"
);
assert!(
lines.iter().any(|l| l.contains("[dev_p,5,0] ")),
"device output prefix should embed addr, got {lines:?}"
);
let _ = std::fs::remove_file(&temp);
}
#[test]
fn output_device_with_no_device_config_reads_the_port_slot() {
let mgr = TraceManager::new();
mgr.register_port("no_overrides", true);
mgr.set_trace_info_mask(Some("no_overrides"), TraceInfoMask::PORT);
mgr.set_trace_mask(Some("no_overrides"), TraceMask::ERROR);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_device_fallback.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(
Some("no_overrides"),
TraceFile::File(Arc::new(Mutex::new(file))),
);
mgr.output_device("no_overrides", Some(0), 0, TraceMask::ERROR, "port-error");
let lines = read_lines(&temp);
assert!(lines.iter().any(|l| l.contains("port-error")));
assert!(lines.iter().any(|l| l.contains("[no_overrides,0,0] ")));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn output_device_with_source_includes_source_when_device_info_mask_has_it() {
let mgr = TraceManager::new();
mgr.register_port("p", true);
mgr.set_trace_mask(Some("p"), TraceMask::ERROR);
mgr.set_device_trace_mask("p", 1, TraceMask::ERROR);
mgr.set_device_trace_info_mask("p", 1, TraceInfoMask::PORT | TraceInfoMask::SOURCE);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_device_source.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_device_trace_file("p", 1, TraceFile::File(Arc::new(Mutex::new(file))));
mgr.output_device_with_source("p", Some(1), 0, TraceMask::ERROR, "src.rs", 42, "msg");
let lines = read_lines(&temp);
assert!(
lines.iter().any(|l| l.contains("[src.rs:42]")),
"device cfg with SOURCE bit should emit `[file:line]` prefix"
);
let _ = std::fs::remove_file(&temp);
}
#[test]
fn output_device_io_uses_device_truncate() {
let mgr = TraceManager::new();
mgr.register_port("p", true);
mgr.set_trace_mask(Some("p"), TraceMask::IO_DRIVER);
mgr.set_io_truncate_size(Some("p"), 64);
mgr.set_device_trace_mask("p", 0, TraceMask::IO_DRIVER);
mgr.set_device_io_truncate_size("p", 0, 3);
mgr.set_device_trace_info_mask("p", 0, TraceInfoMask::PORT);
mgr.set_device_trace_io_mask("p", 0, TraceIoMask::ASCII);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_device_trunc.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_device_trace_file("p", 0, TraceFile::File(Arc::new(Mutex::new(file))));
mgr.output_device_io(
"p",
Some(0),
0,
TraceMask::IO_DRIVER,
b"hello world",
"rx:",
"",
0,
);
let contents = std::fs::read_to_string(&temp).unwrap_or_default();
assert!(contents.contains("hel"));
assert!(!contents.contains("hello"));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn asyn_trace_device_macro_short_circuits_when_device_disabled() {
let mgr = TraceManager::new();
mgr.register_port("p", true);
mgr.set_device_trace_mask("p", 0, TraceMask::empty());
asyn_trace_device!(mgr, "p", 0, TraceMask::ERROR, "should-not-emit");
asyn_trace_device_io!(mgr, "p", 0, TraceMask::ERROR, b"data", "rx:");
}
#[test]
fn asyn_trace_device_macro_emits_when_device_enables_flow() {
let mgr = TraceManager::new();
mgr.register_port("p", true);
mgr.set_trace_mask(Some("p"), TraceMask::ERROR);
mgr.set_device_trace_mask("p", 7, TraceMask::FLOW);
mgr.set_device_trace_info_mask("p", 7, TraceInfoMask::PORT);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_device_macro.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_device_trace_file("p", 7, TraceFile::File(Arc::new(Mutex::new(file))));
asyn_trace_device!(mgr, "p", 7, TraceMask::FLOW, "{}", "device-msg");
let contents = std::fs::read_to_string(&temp).unwrap_or_default();
assert!(contents.contains("device-msg"));
assert!(contents.contains("[p,7,0] "));
let _ = std::fs::remove_file(&temp);
}
#[test]
fn the_ascii_block_stops_at_an_embedded_nul_where_escape_does_not() {
let payload = b"head\0tail";
let cfg = TraceConfig {
trace_io_mask: TraceIoMask::ASCII,
io_truncate_size: 80,
..TraceConfig::default()
};
let mut out = Vec::new();
append_io_data(&mut out, payload, &cfg);
assert_eq!(
out, b"head\n",
"the ASCII block stops at the NUL, got {out:?}"
);
let cfg = TraceConfig {
trace_io_mask: TraceIoMask::ESCAPE,
io_truncate_size: 80,
..TraceConfig::default()
};
let mut out = Vec::new();
append_io_data(&mut out, payload, &cfg);
assert_eq!(out, b"head\\0tail\n", "ESCAPE is a byte loop, got {out:?}");
}
#[test]
fn a_source_only_print_io_emits_the_file_and_line() {
let mgr = TraceManager::new();
mgr.set_trace_mask(Some("p"), TraceMask::IO_DRIVER);
mgr.set_trace_info_mask(Some("p"), TraceInfoMask::SOURCE);
mgr.set_trace_io_mask(Some("p"), TraceIoMask::ASCII);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_io_source.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(Some("p"), TraceFile::File(Arc::new(Mutex::new(file))));
mgr.output_io(
"p",
TraceMask::IO_DRIVER,
b"OK",
"read 2 bytes",
"crates/asyn-rs/src/drivers/serial_port.rs",
1355,
);
let contents = std::fs::read_to_string(&temp).unwrap();
assert!(
contents.starts_with("[serial_port.rs:1355] read 2 bytes\n"),
"SOURCE is the only info bit, so it is the whole prefix: {contents:?}"
);
let _ = std::fs::remove_file(&temp);
}
#[test]
fn the_prefix_is_c_s_four_components_in_c_s_order() {
let mgr = TraceManager::new();
mgr.set_trace_mask(Some("p"), TraceMask::ERROR);
mgr.set_trace_info_mask(
Some("p"),
TraceInfoMask::TIME
| TraceInfoMask::PORT
| TraceInfoMask::SOURCE
| TraceInfoMask::THREAD,
);
let dir = tempfile::tempdir().expect("fixture root");
let temp = dir.path().join("asyn_trace_prefix.txt");
let file = std::fs::File::create(&temp).unwrap();
mgr.set_trace_file(Some("p"), TraceFile::File(Arc::new(Mutex::new(file))));
mgr.output_device_with_source(
"p",
Some(3),
7,
TraceMask::ERROR,
"crates/asyn-rs/src/drivers/ip_port.rs",
871,
"read 2 bytes",
);
let line = read_lines(&temp).remove(0);
let (time, rest) = line.split_at(24);
assert!(
time.as_bytes()[4] == b'/'
&& time.as_bytes()[7] == b'/'
&& time.as_bytes()[10] == b' '
&& time.as_bytes()[13] == b':'
&& time.as_bytes()[16] == b':'
&& time.as_bytes()[19] == b'.'
&& time.as_bytes()[23] == b' ',
"strftime TIME, got {time:?}"
);
assert!(
time[..4].chars().all(|c| c.is_ascii_digit()),
"a four-digit year, not an epoch second count: {time:?}"
);
let rest = rest
.strip_prefix("[p,3,7] ")
.unwrap_or_else(|| panic!("PORT triple, got {rest:?}"));
let rest = rest
.strip_prefix("[ip_port.rs:871] ")
.unwrap_or_else(|| panic!("asynStripPath-ed SOURCE, got {rest:?}"));
let (thread, msg) = rest
.split_once("] ")
.unwrap_or_else(|| panic!("THREAD, got {rest:?}"));
assert!(
thread.starts_with('[') && thread.matches(',').count() == 2,
"THREAD is `[name,id,priority]`, got {thread:?}"
);
assert_eq!(msg, "read 2 bytes");
assert!(!line.contains("ERROR"), "no mask label, got {line:?}");
let _ = std::fs::remove_file(&temp);
}
#[test]
fn set_device_trace_io_mask_writes_device_slot_and_announces() {
let mgr = TraceManager::new();
let em = Arc::new(ExceptionManager::new());
mgr.set_exception_sink(em.clone());
let observed: Arc<Mutex<Vec<(AsynException, i32)>>> = Arc::new(Mutex::new(Vec::new()));
let obs = observed.clone();
em.add_callback(move |ev| {
obs.lock().unwrap().push((ev.exception, ev.addr));
});
mgr.register_port("p", true);
mgr.set_trace_io_mask(Some("p"), TraceIoMask::ASCII);
mgr.set_device_trace_io_mask("p", 4, TraceIoMask::HEX);
assert_eq!(mgr.snapshot("p", Some(4)).io_mask, TraceIoMask::HEX);
assert_eq!(mgr.snapshot("p", None).io_mask, TraceIoMask::ASCII);
let events = observed.lock().unwrap();
assert!(
events
.iter()
.any(|(e, a)| matches!(e, AsynException::TraceIoMask) && *a == 4)
);
}
#[test]
fn set_device_trace_info_mask_writes_device_slot_and_announces() {
let mgr = TraceManager::new();
let em = Arc::new(ExceptionManager::new());
mgr.set_exception_sink(em.clone());
let observed: Arc<Mutex<Vec<(AsynException, i32)>>> = Arc::new(Mutex::new(Vec::new()));
let obs = observed.clone();
em.add_callback(move |ev| {
obs.lock().unwrap().push((ev.exception, ev.addr));
});
mgr.register_port("p", true);
mgr.set_device_trace_info_mask("p", 2, TraceInfoMask::SOURCE | TraceInfoMask::TIME);
assert_eq!(
mgr.snapshot("p", Some(2)).info_mask,
TraceInfoMask::SOURCE | TraceInfoMask::TIME
);
let events = observed.lock().unwrap();
assert!(
events
.iter()
.any(|(e, a)| matches!(e, AsynException::TraceInfoMask) && *a == 2)
);
}
#[test]
fn set_device_trace_file_routes_emit_to_device_sink() {
let mgr = TraceManager::new();
let em = Arc::new(ExceptionManager::new());
mgr.set_exception_sink(em.clone());
let observed: Arc<Mutex<Vec<(AsynException, i32)>>> = Arc::new(Mutex::new(Vec::new()));
let obs = observed.clone();
em.add_callback(move |ev| {
obs.lock().unwrap().push((ev.exception, ev.addr));
});
mgr.register_port("p", true);
mgr.set_trace_mask(Some("p"), TraceMask::ERROR);
mgr.set_trace_info_mask(Some("p"), TraceInfoMask::PORT);
mgr.set_device_trace_mask("p", 3, TraceMask::ERROR);
mgr.set_device_trace_info_mask("p", 3, TraceInfoMask::PORT);
let dir = tempfile::tempdir().expect("fixture root");
let port_temp = dir.path().join("asyn_trace_dev_file_port.txt");
let dev_temp = dir.path().join("asyn_trace_dev_file_dev.txt");
let port_f = std::fs::File::create(&port_temp).unwrap();
let dev_f = std::fs::File::create(&dev_temp).unwrap();
mgr.set_trace_file(Some("p"), TraceFile::File(Arc::new(Mutex::new(port_f))));
mgr.set_device_trace_file("p", 3, TraceFile::File(Arc::new(Mutex::new(dev_f))));
mgr.output_device("p", Some(3), 0, TraceMask::ERROR, "device-only-msg");
let dev_lines = read_lines(&dev_temp);
assert!(dev_lines.iter().any(|l| l.contains("device-only-msg")));
let port_lines = read_lines(&port_temp);
assert!(
!port_lines.iter().any(|l| l.contains("device-only-msg")),
"addr-targeted emit must not write to port sink"
);
let events = observed.lock().unwrap();
assert!(
events
.iter()
.any(|(e, a)| matches!(e, AsynException::TraceFile) && *a == 3)
);
let _ = std::fs::remove_file(&port_temp);
let _ = std::fs::remove_file(&dev_temp);
}
#[test]
fn a_port_level_mask_set_pushes_down_and_quiets_a_louder_device() {
let mgr = TraceManager::new();
mgr.register_port("P", true);
mgr.set_device_trace_mask("P", 1, TraceMask::ERROR | TraceMask::FLOW);
assert!(mgr.is_enabled_device("P", 1, TraceMask::FLOW));
mgr.set_trace_mask(Some("P"), TraceMask::ERROR);
assert!(
!mgr.is_enabled_device("P", 1, TraceMask::FLOW),
"the port-level set must overwrite device 1's slot"
);
assert!(mgr.is_enabled_device("P", 1, TraceMask::ERROR));
}
#[test]
fn a_global_mask_set_does_not_reach_a_port() {
let mgr = TraceManager::new();
mgr.register_port("Q", false);
mgr.set_trace_mask(None, TraceMask::ERROR | TraceMask::FLOW);
assert!(
!mgr.is_enabled("Q", TraceMask::FLOW),
"a global set must not appear on a port's dpCommon"
);
assert!(mgr.is_enabled("Q", TraceMask::ERROR), "born with ERROR");
assert!(mgr.get_trace_mask(None).contains(TraceMask::FLOW));
}
#[test]
fn an_address_on_a_single_device_port_names_the_port_not_a_device() {
let mgr = TraceManager::new();
mgr.register_port("S", false);
mgr.set_device_trace_mask("S", 3, TraceMask::ERROR | TraceMask::FLOW);
assert!(
mgr.is_enabled("S", TraceMask::FLOW),
"the write landed on the port, as C's NULL pdevice makes it"
);
mgr.set_trace_mask(Some("S"), TraceMask::ERROR);
assert!(!mgr.is_enabled_device("S", 3, TraceMask::FLOW));
mgr.register_port("M", true);
mgr.set_trace_mask(Some("M"), TraceMask::ERROR);
mgr.set_device_trace_mask("M", 3, TraceMask::ERROR | TraceMask::FLOW);
assert!(mgr.is_enabled_device("M", 3, TraceMask::FLOW));
assert!(
!mgr.is_enabled("M", TraceMask::FLOW),
"and the port's own slot is untouched by it"
);
}
#[test]
fn a_port_level_set_announces_per_device_then_for_the_port() {
let mgr = TraceManager::new();
let em = Arc::new(ExceptionManager::new());
mgr.set_exception_sink(em.clone());
mgr.register_port("P", true);
mgr.set_device_trace_mask("P", 1, TraceMask::ERROR);
mgr.set_device_trace_mask("P", 2, TraceMask::ERROR);
let observed: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new()));
let obs = observed.clone();
em.add_callback(move |ev| {
if matches!(ev.exception, AsynException::TraceMask) {
obs.lock().unwrap().push(ev.addr);
}
});
mgr.set_trace_mask(Some("P"), TraceMask::ERROR | TraceMask::FLOW);
assert_eq!(*observed.lock().unwrap(), vec![1, 2, -1]);
}
}