#[cfg(all(feature = "log", not(feature = "tracing")))]
use log::{debug, error, info, trace, warn};
use std::{
fmt::Display,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OperationResult {
Suc,
#[default]
Fail,
Cancel,
}
const DEFAULT_MOD_PATH: &str = module_path!();
#[macro_export]
macro_rules! op_context {
($target:expr) => {
$crate::OperationContext::want($target).with_mod_path(module_path!())
};
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OperationContext {
context: CallContext,
result: OperationResult,
exit_log: bool,
mod_path: String,
target: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
path: Vec<String>,
}
impl Default for OperationContext {
fn default() -> Self {
Self {
context: CallContext::default(),
target: None,
path: Vec::new(),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
pub type WithContext = OperationContext;
impl From<CallContext> for OperationContext {
fn from(value: CallContext) -> Self {
OperationContext {
context: value,
result: OperationResult::Fail,
target: None,
path: Vec::new(),
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl Drop for OperationContext {
fn drop(&mut self) {
if !self.exit_log {
return;
}
#[cfg(feature = "tracing")]
{
let ctx = self.format_context();
match self.result() {
OperationResult::Suc => {
tracing::info!(
target: "domain",
mod_path = %self.mod_path,
"suc! {ctx}"
)
}
OperationResult::Fail => {
tracing::error!(
target: "domain",
mod_path = %self.mod_path,
"fail! {ctx}"
)
}
OperationResult::Cancel => {
tracing::warn!(
target: "domain",
mod_path = %self.mod_path,
"cancel! {ctx}"
)
}
}
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
{
match self.result() {
OperationResult::Suc => {
info!(target: self.mod_path.as_str(), "suc! {}", self.format_context());
}
OperationResult::Fail => {
error!(target: self.mod_path.as_str(), "fail! {}", self.format_context());
}
OperationResult::Cancel => {
warn!(target: self.mod_path.as_str(), "cancel! {}", self.format_context());
}
}
}
}
}
impl Display for OperationContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(target) = &self.target {
writeln!(f, "want: {target}")?;
}
if let Some(path) = self.path_string() {
if self.target.as_deref() != Some(path.as_str()) {
writeln!(f, "path: {path}")?;
}
}
for (i, (k, v)) in self.context().items.iter().enumerate() {
writeln!(f, "{}. {k}: {v} ", i + 1)?;
}
Ok(())
}
}
pub trait ContextRecord<S1, S2> {
fn record(&mut self, key: S1, val: S2);
}
impl<S1> ContextRecord<S1, String> for OperationContext
where
S1: Into<String>,
{
fn record(&mut self, key: S1, val: String) {
self.context.items.push((key.into(), val));
}
}
impl<S1> ContextRecord<S1, &str> for OperationContext
where
S1: Into<String>,
{
fn record(&mut self, key: S1, val: &str) {
self.context.items.push((key.into(), val.into()));
}
}
impl<S1> ContextRecord<S1, &PathBuf> for OperationContext
where
S1: Into<String>,
{
fn record(&mut self, key: S1, val: &PathBuf) {
self.context
.items
.push((key.into(), format!("{}", val.display())));
}
}
impl<S1> ContextRecord<S1, &Path> for OperationContext
where
S1: Into<String>,
{
fn record(&mut self, key: S1, val: &Path) {
self.context
.items
.push((key.into(), format!("{}", val.display())));
}
}
impl OperationContext {
pub fn context(&self) -> &CallContext {
&self.context
}
pub fn result(&self) -> &OperationResult {
&self.result
}
pub fn exit_log(&self) -> &bool {
&self.exit_log
}
pub fn mod_path(&self) -> &String {
&self.mod_path
}
pub fn target(&self) -> &Option<String> {
&self.target
}
pub fn path(&self) -> &[String] {
&self.path
}
pub fn new() -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::default(),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
pub fn want<S: Into<String>>(target: S) -> Self {
let target = target.into();
Self {
target: Some(target.clone()),
path: vec![target],
context: CallContext::default(),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
#[deprecated(since = "0.5.4", note = "use with_auto_log")]
pub fn with_exit_log(mut self) -> Self {
self.exit_log = true;
self
}
pub fn with_auto_log(mut self) -> Self {
self.exit_log = true;
self
}
pub fn with_mod_path<S: Into<String>>(mut self, path: S) -> Self {
self.mod_path = path.into();
self
}
#[deprecated(since = "0.5.4", note = "use record")]
pub fn with<S1: Into<String>, S2: Into<String>>(&mut self, key: S1, val: S2) {
self.context.items.push((key.into(), val.into()));
}
#[deprecated(since = "0.5.4", note = "use record")]
pub fn with_path<S1: Into<String>, S2: Into<PathBuf>>(&mut self, key: S1, val: S2) {
self.context
.items
.push((key.into(), format!("{}", val.into().display())));
}
pub fn with_want<S: Into<String>>(&mut self, target: S) {
let target = target.into();
if target.is_empty() {
return;
}
if let Some(root) = &self.target {
if self.path.is_empty() {
self.path.push(root.clone());
}
if self.path.last() != Some(&target) {
self.path.push(target);
}
} else {
self.target = Some(target.clone());
self.path.push(target);
}
}
pub fn set_target<S: Into<String>>(&mut self, target: S) {
self.with_want(target)
}
pub fn path_string(&self) -> Option<String> {
if self.path.is_empty() {
None
} else {
Some(self.path.join(" / "))
}
}
pub fn mark_suc(&mut self) {
self.result = OperationResult::Suc;
}
pub fn mark_cancel(&mut self) {
self.result = OperationResult::Cancel;
}
#[cfg_attr(not(any(feature = "log", feature = "tracing")), allow(dead_code))]
fn format_context(&self) -> String {
let want = self.target.clone().unwrap_or_default();
let path = self.path_string().unwrap_or_default();
let head = match (want.is_empty(), path.is_empty() || path == want) {
(true, true) => String::new(),
(false, true) => format!("want={want}"),
(false, false) => format!("want={want} path={path}"),
(true, false) => format!("path={path}"),
};
if self.context.items.is_empty() {
return head;
}
if head.is_empty() {
let body = self.context.to_string();
body.strip_prefix('\n').unwrap_or(&body).to_string()
} else {
format!("{head}: {}", self.context)
}
}
pub fn scope(&mut self) -> OperationScope<'_> {
OperationScope {
ctx: self,
mark_success: false,
}
}
pub fn scoped_success(&mut self) -> OperationScope<'_> {
OperationScope {
ctx: self,
mark_success: true,
}
}
#[cfg(feature = "tracing")]
pub fn info<S: AsRef<str>>(&self, message: S) {
tracing::info!(
target: "domain",
mod_path = %self.mod_path,
"{}: {}",
self.format_context(),
message.as_ref()
);
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
pub fn info<S: AsRef<str>>(&self, message: S) {
info!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
}
#[cfg(not(any(feature = "log", feature = "tracing")))]
pub fn info<S: AsRef<str>>(&self, _message: S) {}
#[cfg(feature = "tracing")]
pub fn debug<S: AsRef<str>>(&self, message: S) {
tracing::debug!(
target: "domain",
mod_path = %self.mod_path,
"{}: {}",
self.format_context(),
message.as_ref()
);
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
pub fn debug<S: AsRef<str>>(&self, message: S) {
debug!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
}
#[cfg(not(any(feature = "log", feature = "tracing")))]
pub fn debug<S: AsRef<str>>(&self, _message: S) {}
#[cfg(feature = "tracing")]
pub fn warn<S: AsRef<str>>(&self, message: S) {
tracing::warn!(
target: "domain",
mod_path = %self.mod_path,
"{}: {}",
self.format_context(),
message.as_ref()
);
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
pub fn warn<S: AsRef<str>>(&self, message: S) {
warn!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
}
#[cfg(not(any(feature = "log", feature = "tracing")))]
pub fn warn<S: AsRef<str>>(&self, _message: S) {}
#[cfg(feature = "tracing")]
pub fn error<S: AsRef<str>>(&self, message: S) {
tracing::error!(
target: "domain",
mod_path = %self.mod_path,
"{}: {}",
self.format_context(),
message.as_ref()
);
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
pub fn error<S: AsRef<str>>(&self, message: S) {
error!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
}
#[cfg(not(any(feature = "log", feature = "tracing")))]
pub fn error<S: AsRef<str>>(&self, _message: S) {}
#[cfg(feature = "tracing")]
pub fn trace<S: AsRef<str>>(&self, message: S) {
tracing::trace!(
target: "domain",
mod_path = %self.mod_path,
"{}: {}",
self.format_context(),
message.as_ref()
);
}
#[cfg(all(feature = "log", not(feature = "tracing")))]
pub fn trace<S: AsRef<str>>(&self, message: S) {
trace!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
}
#[cfg(not(any(feature = "log", feature = "tracing")))]
pub fn trace<S: AsRef<str>>(&self, _message: S) {}
pub fn log_info<S: AsRef<str>>(&self, message: S) {
self.info(message)
}
pub fn log_debug<S: AsRef<str>>(&self, message: S) {
self.debug(message)
}
pub fn log_warn<S: AsRef<str>>(&self, message: S) {
self.warn(message)
}
pub fn log_error<S: AsRef<str>>(&self, message: S) {
self.error(message)
}
pub fn log_trace<S: AsRef<str>>(&self, message: S) {
self.trace(message)
}
}
pub struct OperationScope<'a> {
ctx: &'a mut OperationContext,
mark_success: bool,
}
impl<'a> OperationScope<'a> {
pub fn mark_success(&mut self) {
self.mark_success = true;
}
pub fn mark_failure(&mut self) {
self.mark_success = false;
}
pub fn cancel(&mut self) {
self.ctx.mark_cancel();
self.mark_success = false;
}
}
impl<'a> Deref for OperationScope<'a> {
type Target = OperationContext;
fn deref(&self) -> &Self::Target {
self.ctx
}
}
impl<'a> DerefMut for OperationScope<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ctx
}
}
impl Drop for OperationScope<'_> {
fn drop(&mut self) {
if self.mark_success {
self.ctx.mark_suc();
}
}
}
impl From<String> for OperationContext {
fn from(value: String) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from(("key", value.to_string())),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<&PathBuf> for OperationContext {
fn from(value: &PathBuf) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from(("path", format!("{}", value.display()))),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<&Path> for OperationContext {
fn from(value: &Path) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from(("path", format!("{}", value.display()))),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<&str> for OperationContext {
fn from(value: &str) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from(("key", value.to_string())),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<(&str, &str)> for OperationContext {
fn from(value: (&str, &str)) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from((value.0, value.1)),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<(&str, String)> for OperationContext {
fn from(value: (&str, String)) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from((value.0, value.1)),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
trait NotAsRefStr: AsRef<Path> {}
impl NotAsRefStr for PathBuf {}
impl NotAsRefStr for Path {}
impl<T: AsRef<Path> + ?Sized> NotAsRefStr for &T where T: NotAsRefStr {}
impl<V: AsRef<Path>> From<(&str, V)> for OperationContext
where
V: NotAsRefStr,
{
fn from(value: (&str, V)) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext {
items: vec![(
value.0.to_string(),
format!("{}", value.1.as_ref().display()),
)],
},
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<(String, String)> for OperationContext {
fn from(value: (String, String)) -> Self {
Self {
target: None,
path: Vec::new(),
context: CallContext::from((value.0, value.1)),
result: OperationResult::Fail,
exit_log: false,
mod_path: DEFAULT_MOD_PATH.into(),
}
}
}
impl From<&OperationContext> for OperationContext {
fn from(value: &OperationContext) -> Self {
value.clone()
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CallContext {
pub items: Vec<(String, String)>,
}
impl<K: AsRef<str>, V: AsRef<str>> From<(K, V)> for CallContext {
fn from(value: (K, V)) -> Self {
Self {
items: vec![(value.0.as_ref().to_string(), value.1.as_ref().to_string())],
}
}
}
pub trait ContextAdd<T> {
fn add_context(&mut self, val: T);
}
impl<K: Into<String>> ContextAdd<(K, String)> for OperationContext {
fn add_context(&mut self, val: (K, String)) {
self.record(val.0.into(), val.1);
}
}
impl<K: Into<String>> ContextAdd<(K, &String)> for OperationContext {
fn add_context(&mut self, val: (K, &String)) {
self.record(val.0.into(), val.1.clone());
}
}
impl<K: Into<String>> ContextAdd<(K, &str)> for OperationContext {
fn add_context(&mut self, val: (K, &str)) {
self.record(val.0.into(), val.1.to_string());
}
}
impl<K: Into<String>> ContextAdd<(K, &PathBuf)> for OperationContext {
fn add_context(&mut self, val: (K, &PathBuf)) {
self.record(val.0.into(), format!("{}", val.1.display()));
}
}
impl<K: Into<String>> ContextAdd<(K, &Path)> for OperationContext {
fn add_context(&mut self, val: (K, &Path)) {
self.record(val.0.into(), format!("{}", val.1.display()));
}
}
impl Display for CallContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.items.is_empty() {
writeln!(f, "\ncall context:")?;
}
for (k, v) in &self.items {
writeln!(f, "\t{k} : {v}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_op_context_macro_sets_callsite_mod_path() {
let ctx = crate::op_context!("macro_target");
assert_eq!(*ctx.target(), Some("macro_target".to_string()));
assert_eq!(ctx.mod_path().as_str(), module_path!());
}
#[test]
fn test_withcontext_new() {
let ctx = OperationContext::new();
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 0);
}
#[test]
fn test_withcontext_want() {
let ctx = OperationContext::want("test_target");
assert_eq!(*ctx.target(), Some("test_target".to_string()));
assert_eq!(ctx.path(), &["test_target".to_string()]);
assert_eq!(ctx.context().items.len(), 0);
}
#[test]
fn test_withcontext_with() {
let mut ctx = OperationContext::new();
ctx.record("key1", "value1");
ctx.record("key2", "value2");
assert_eq!(ctx.context().items.len(), 2);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
assert_eq!(
ctx.context().items[1],
("key2".to_string(), "value2".to_string())
);
}
#[test]
fn test_withcontext_with_path() {
let mut ctx = OperationContext::new();
let path = PathBuf::from("/test/path");
ctx.record("file_path", &path);
assert_eq!(ctx.context().items.len(), 1);
assert!(ctx.context().items[0].1.contains("/test/path"));
}
#[test]
fn test_withcontext_with_want() {
let mut ctx = OperationContext::new();
ctx.with_want("new_target");
assert_eq!(*ctx.target(), Some("new_target".to_string()));
assert_eq!(ctx.path(), &["new_target".to_string()]);
}
#[test]
fn test_withcontext_with_want_appends_path() {
let mut ctx = OperationContext::want("place_order");
ctx.with_want("read_order_payload");
ctx.with_want("parse_order");
assert_eq!(*ctx.target(), Some("place_order".to_string()));
assert_eq!(
ctx.path(),
&[
"place_order".to_string(),
"read_order_payload".to_string(),
"parse_order".to_string()
]
);
assert_eq!(
ctx.path_string().as_deref(),
Some("place_order / read_order_payload / parse_order")
);
}
#[test]
fn test_errcontext_from_string() {
let ctx = CallContext::from(("key".to_string(), "test_string".to_string()));
assert_eq!(ctx.items.len(), 1);
assert_eq!(ctx.items[0], ("key".to_string(), "test_string".to_string()));
}
#[test]
fn test_errcontext_from_str() {
let ctx = CallContext::from(("key", "test_str"));
assert_eq!(ctx.items.len(), 1);
assert_eq!(ctx.items[0], ("key".to_string(), "test_str".to_string()));
}
#[test]
fn test_errcontext_from_string_pair() {
let ctx = CallContext::from(("key1".to_string(), "value1".to_string()));
assert_eq!(ctx.items.len(), 1);
assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
}
#[test]
fn test_errcontext_from_str_pair() {
let ctx = CallContext::from(("key1", "value1"));
assert_eq!(ctx.items.len(), 1);
assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
}
#[test]
fn test_errcontext_from_mixed_pair() {
let ctx = CallContext::from(("key1", "value1".to_string()));
assert_eq!(ctx.items.len(), 1);
assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
}
#[test]
fn test_errcontext_default() {
let ctx = CallContext::default();
assert_eq!(ctx.items.len(), 0);
}
#[test]
fn test_errcontext_display_single() {
let ctx = CallContext::from(("key", "test"));
let display = format!("{ctx}");
assert!(display.contains("call context:"));
assert!(display.contains("key : test"));
}
#[test]
fn test_errcontext_display_multiple() {
let mut ctx = CallContext::default();
ctx.items.push(("key1".to_string(), "value1".to_string()));
ctx.items.push(("key2".to_string(), "value2".to_string()));
let display = format!("{ctx}");
assert!(display.contains("call context:"));
assert!(display.contains("key1 : value1"));
assert!(display.contains("key2 : value2"));
}
#[test]
fn test_errcontext_display_empty() {
let ctx = CallContext::default();
let display = format!("{ctx}");
assert_eq!(display, "");
}
#[test]
fn test_withcontext_from_string() {
let ctx = OperationContext::from("test_string".to_string());
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key".to_string(), "test_string".to_string())
);
}
#[test]
fn test_withcontext_from_str() {
let ctx = OperationContext::from("test_str".to_string());
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key".to_string(), "test_str".to_string())
);
}
#[test]
fn test_withcontext_from_pathbuf() {
let path = PathBuf::from("/test/path");
let ctx = OperationContext::from(&path);
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert!(ctx.context().items[0].1.contains("/test/path"));
}
#[test]
fn test_withcontext_from_path() {
let path = "/test/path";
let ctx = OperationContext::from(path);
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert!(ctx.context().items[0].1.contains("/test/path"));
}
#[test]
fn test_withcontext_from_string_pair() {
let ctx = OperationContext::from(("key1".to_string(), "value1".to_string()));
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
}
#[test]
fn test_withcontext_from_str_pair() {
let ctx = OperationContext::from(("key1", "value1"));
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
}
#[test]
fn test_withcontext_from_mixed_pair() {
let ctx = OperationContext::from(("key1", "value1".to_string()));
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
}
#[test]
fn test_withcontext_from_path_pair() {
let path = PathBuf::from("/test/path");
let ctx = OperationContext::from(("file", path.to_string_lossy().as_ref()));
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert!(ctx.context().items[0].0.contains("file"));
assert!(ctx.context().items[0].1.contains("/test/path"));
}
#[test]
fn test_withcontext_display_with_target() {
let mut ctx = OperationContext::want("test_target");
ctx.record("key1", "value1");
let display = format!("{ctx}");
assert!(display.contains("want: test_target"));
assert!(display.contains("1. key1: value1"));
}
#[test]
fn test_withcontext_display_without_target() {
let mut ctx = OperationContext::new();
ctx.record("key1", "value1");
let display = format!("{ctx}");
assert!(!display.contains("target:"));
assert!(display.contains("1. key1: value1"));
}
#[test]
fn test_withcontext_from_errcontext() {
let err_ctx = CallContext::from(("key1", "value1"));
let ctx = OperationContext::from(err_ctx);
assert!(ctx.target.is_none());
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
}
#[test]
fn test_withcontext_from_withcontext() {
let mut ctx1 = OperationContext::want("target1");
ctx1.record("key1", "value1");
ctx1.with_want("step1");
let ctx2 = OperationContext::from(&ctx1);
assert_eq!(*ctx2.target(), Some("target1".to_string()));
assert_eq!(ctx2.path(), &["target1".to_string(), "step1".to_string()]);
assert_eq!(ctx2.context().items.len(), 1);
assert_eq!(
ctx2.context().items[0],
("key1".to_string(), "value1".to_string())
);
}
#[test]
fn test_withcontext_from_str_path_pair() {
let path = PathBuf::from("/test/path");
let ctx = OperationContext::from(("file", &path));
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(ctx.context().items[0].0, "file");
assert!(ctx.context().items[0].1.contains("/test/path"));
}
#[test]
fn test_withcontext_from_str_pathbuf_pair() {
let path = PathBuf::from("/test/pathbuf");
let ctx = OperationContext::from(("file", path));
assert_eq!(ctx.context().items.len(), 1);
assert_eq!(ctx.context().items[0].0, "file");
assert!(ctx.context().items[0].1.contains("/test/pathbuf"));
}
#[test]
fn test_withcontext_edge_cases() {
let ctx1 = OperationContext::from("".to_string());
assert_eq!(ctx1.context().items.len(), 1);
assert_eq!(ctx1.context().items[0], ("key".to_string(), "".to_string()));
let ctx2 = OperationContext::from(("".to_string(), "".to_string()));
assert_eq!(ctx2.context().items.len(), 1);
assert_eq!(ctx2.context().items[0], ("".to_string(), "".to_string()));
}
#[test]
fn test_errcontext_equality() {
let ctx1 = CallContext::from(("key1", "value1"));
let ctx2 = CallContext::from(("key1", "value1"));
let ctx3 = CallContext::from(("key1", "value2"));
assert_eq!(ctx1, ctx2);
assert_ne!(ctx1, ctx3);
}
#[test]
fn test_withcontext_equality() {
let ctx1 = OperationContext::from(("key1", "value1"));
let ctx2 = OperationContext::from(("key1", "value1"));
let ctx3 = OperationContext::from(("key1", "value2"));
assert_eq!(ctx1, ctx2);
assert_ne!(ctx1, ctx3);
}
#[test]
fn test_withcontext_clone() {
let mut ctx = OperationContext::want("target");
ctx.record("key", "value");
let cloned = ctx.clone();
assert_eq!(ctx.target(), cloned.target());
assert_eq!(ctx.context().items.len(), cloned.context().items.len());
assert_eq!(ctx.context().items[0], cloned.context().items[0]);
}
#[test]
fn test_withcontext_with_types() {
let mut ctx = OperationContext::new();
ctx.record("string_key", "string_value");
ctx.record("string_key", 42.to_string()); ctx.record("bool_key", true.to_string());
assert_eq!(ctx.context().items.len(), 3);
assert_eq!(
ctx.context().items[2],
("bool_key".to_string(), "true".to_string())
);
}
#[test]
fn test_mark_suc() {
let mut ctx = OperationContext::new();
assert!(ctx.result == OperationResult::Fail);
ctx.mark_suc();
assert!(ctx.result == OperationResult::Suc);
}
#[test]
fn test_with_exit_log() {
let ctx = OperationContext::new().with_auto_log();
assert!(ctx.exit_log);
let ctx2 = OperationContext::want("test").with_auto_log();
assert!(ctx2.exit_log);
assert_eq!(*ctx2.target(), Some("test".to_string()));
}
#[test]
fn test_scope_marks_success() {
let mut ctx = OperationContext::want("scope_success");
{
let _scope = ctx.scoped_success();
}
assert!(matches!(ctx.result(), OperationResult::Suc));
}
#[test]
fn test_scope_preserves_failure() {
let mut ctx = OperationContext::want("scope_fail");
{
let mut scope = ctx.scoped_success();
scope.mark_failure();
}
assert!(matches!(ctx.result(), OperationResult::Fail));
}
#[test]
fn test_scope_cancel() {
let mut ctx = OperationContext::want("scope_cancel");
{
let mut scope = ctx.scoped_success();
scope.cancel();
}
assert!(matches!(ctx.result(), OperationResult::Cancel));
}
#[test]
fn test_format_context_with_target() {
let mut ctx = OperationContext::want("test_target");
ctx.record("key1", "value1");
let formatted = ctx.format_context();
assert_eq!(
formatted,
"want=test_target: \ncall context:\n\tkey1 : value1\n"
);
}
#[test]
fn test_format_context_without_target() {
let mut ctx = OperationContext::new();
ctx.record("key1", "value1");
let formatted = ctx.format_context();
assert_eq!(formatted, "call context:\n\tkey1 : value1\n");
}
#[test]
fn test_format_context_empty() {
let ctx = OperationContext::new();
let formatted = ctx.format_context();
assert_eq!(formatted, "");
}
#[test]
fn test_format_context_with_target_only() {
let ctx = OperationContext::want("test_target");
let formatted = ctx.format_context();
assert_eq!(formatted, "want=test_target");
}
#[test]
fn test_format_context_with_path() {
let mut ctx = OperationContext::want("place_order");
ctx.with_want("read_order_payload");
ctx.record("order_id", "42");
let formatted = ctx.format_context();
assert_eq!(
formatted,
"want=place_order path=place_order / read_order_payload: \ncall context:\n\torder_id : 42\n"
);
}
#[test]
fn test_logging_methods() {
let ctx = OperationContext::want("test_target");
ctx.info("info message");
ctx.debug("debug message");
ctx.warn("warn message");
ctx.error("error message");
ctx.trace("trace message");
}
#[test]
fn test_logging_methods_with_empty_context() {
let ctx = OperationContext::new();
ctx.info("info message");
ctx.debug("debug message");
ctx.warn("warn message");
ctx.error("error message");
ctx.trace("trace message");
}
#[test]
fn test_context_add_trait() {
let mut ctx = OperationContext::new();
ctx.add_context(("key1", "value1"));
ctx.add_context(("key2", "value2"));
assert_eq!(ctx.context().items.len(), 2);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
assert_eq!(
ctx.context().items[1],
("key2".to_string(), "value2".to_string())
);
}
#[test]
fn test_drop_trait_with_success() {
{
let mut ctx = OperationContext::want("test_drop").with_auto_log();
ctx.record("operation", "test");
ctx.mark_suc(); }
}
#[test]
fn test_drop_trait_with_failure() {
{
let mut ctx = OperationContext::want("test_drop_fail").with_auto_log();
ctx.record("operation", "test_fail");
}
}
#[test]
fn test_drop_trait_without_exit_log() {
{
let mut ctx = OperationContext::want("test_no_log");
ctx.record("operation", "no_log");
ctx.mark_suc();
}
}
#[test]
fn test_complex_context_scenario() {
let mut ctx = OperationContext::want("user_registration").with_auto_log();
ctx.record("user_id", "12345");
ctx.record("email", "test@example.com");
ctx.record("role", "user");
ctx.info("开始用户注册流程");
ctx.debug("验证用户输入");
ctx.warn("检测到潜在的安全风险");
ctx.mark_suc();
ctx.info("用户注册成功");
assert!(ctx.result == OperationResult::Suc);
assert!(ctx.exit_log);
assert_eq!(*ctx.target(), Some("user_registration".to_string()));
assert_eq!(ctx.context().items.len(), 3);
let formatted = ctx.format_context();
assert!(formatted.contains("user_registration"));
assert!(formatted.contains("user_id"));
assert!(formatted.contains("email"));
assert!(formatted.contains("role"));
}
#[cfg(feature = "serde")]
#[test]
fn test_context_serialization() {
let mut ctx = OperationContext::want("serialization_test");
ctx.with_want("inner_step");
ctx.record("key1", "value1");
ctx.record("key2", "value2");
let serialized = serde_json::to_string(&ctx).expect("序列化失败");
assert!(serialized.contains("serialization_test"));
assert!(serialized.contains("inner_step"));
assert!(serialized.contains("key1"));
assert!(serialized.contains("value1"));
let deserialized: OperationContext =
serde_json::from_str(&serialized).expect("反序列化失败");
assert_eq!(ctx, deserialized);
}
#[test]
fn test_context_with_special_characters() {
let mut ctx = OperationContext::new();
ctx.record("key_with_spaces", "value with spaces");
ctx.record("key_with_unicode", "值包含中文");
ctx.record("key_with_symbols", "value@#$%^&*()");
assert_eq!(ctx.context().items.len(), 3);
assert_eq!(
ctx.context().items[0],
(
"key_with_spaces".to_string(),
"value with spaces".to_string()
)
);
assert_eq!(
ctx.context().items[1],
("key_with_unicode".to_string(), "值包含中文".to_string())
);
assert_eq!(
ctx.context().items[2],
("key_with_symbols".to_string(), "value@#$%^&*()".to_string())
);
let display = format!("{ctx}");
assert!(display.contains("key_with_spaces"));
assert!(display.contains("值包含中文"));
assert!(display.contains("value@#$%^&*()"));
}
#[test]
fn test_context_builder_pattern() {
let ctx = OperationContext::want("builder_test").with_auto_log();
assert_eq!(*ctx.target(), Some("builder_test".to_string()));
assert_eq!(ctx.path(), &["builder_test".to_string()]);
assert!(ctx.exit_log);
}
#[test]
fn test_context_multiple_with_calls() {
let mut ctx = OperationContext::new();
ctx.record("key1", "value1");
ctx.record("key2", "value2");
ctx.record("key3", "value3");
ctx.record("key1", "new_value1");
assert_eq!(ctx.context().items.len(), 4);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
assert_eq!(
ctx.context().items[3],
("key1".to_string(), "new_value1".to_string())
);
}
#[test]
fn test_context_from_various_types() {
let ctx1 = OperationContext::from("simple_string");
assert_eq!(
ctx1.context().items[0],
("key".to_string(), "simple_string".to_string())
);
let ctx2 = OperationContext::from(("custom_key", "custom_value"));
assert_eq!(
ctx2.context().items[0],
("custom_key".to_string(), "custom_value".to_string())
);
let path = PathBuf::from("/test/path/file.txt");
let ctx3 = OperationContext::from(&path);
assert!(ctx3.context().items[0].0.contains("path"));
assert!(ctx3.context().items[0].1.contains("/test/path/file.txt"));
}
#[test]
fn test_context_take_with_string_types() {
let mut ctx = OperationContext::new();
ctx.record("string_key", "string_value");
ctx.record("string_key2", String::from("string_value2"));
ctx.record(String::from("string_key3"), "string_value3");
ctx.record(String::from("string_key4"), String::from("string_value4"));
assert_eq!(ctx.context().items.len(), 4);
assert_eq!(
ctx.context().items[0],
("string_key".to_string(), "string_value".to_string())
);
assert_eq!(
ctx.context().items[1],
("string_key2".to_string(), "string_value2".to_string())
);
assert_eq!(
ctx.context().items[2],
("string_key3".to_string(), "string_value3".to_string())
);
assert_eq!(
ctx.context().items[3],
("string_key4".to_string(), "string_value4".to_string())
);
}
#[test]
fn test_context_take_with_numeric_types() {
let mut ctx = OperationContext::new();
ctx.record("int_key", 42.to_string());
ctx.record("float_key", 3.24.to_string());
ctx.record("bool_key", true.to_string());
assert_eq!(ctx.context().items.len(), 3);
assert_eq!(
ctx.context().items[0],
("int_key".to_string(), "42".to_string())
);
assert_eq!(
ctx.context().items[1],
("float_key".to_string(), "3.24".to_string())
);
assert_eq!(
ctx.context().items[2],
("bool_key".to_string(), "true".to_string())
);
}
#[test]
fn test_context_take_with_path_context() {
let mut ctx = OperationContext::new();
let path1 = PathBuf::from("/test/path1.txt");
let path2 = Path::new("/test/path2.txt");
ctx.record("file1", &path1);
ctx.record("file2", path2);
assert_eq!(ctx.context().items.len(), 2);
assert_eq!(ctx.context().items[0].0, "file1");
assert!(ctx.context().items[0].1.contains("/test/path1.txt"));
assert_eq!(ctx.context().items[1].0, "file2");
assert!(ctx.context().items[1].1.contains("/test/path2.txt"));
}
#[test]
fn test_context_take_mixed_types() {
let mut ctx = OperationContext::new();
ctx.record("name", "test_user");
ctx.record("age", 25.to_string());
ctx.record("config_file", &PathBuf::from("/etc/config.toml"));
ctx.record("status", "active");
assert_eq!(ctx.context().items.len(), 4);
assert_eq!(
ctx.context().items[0],
("name".to_string(), "test_user".to_string())
);
assert_eq!(
ctx.context().items[1],
("age".to_string(), "25".to_string())
);
assert_eq!(ctx.context().items[2].0, "config_file");
assert!(ctx.context().items[2].1.contains("/etc/config.toml"));
assert_eq!(
ctx.context().items[3],
("status".to_string(), "active".to_string())
);
}
#[test]
fn test_context_take_edge_cases() {
let mut ctx = OperationContext::new();
ctx.record("", ""); ctx.record("empty_value", ""); ctx.record("", "empty_key"); ctx.record("special_chars", "@#$%^&*()"); ctx.record("unicode", "测试中文字符");
assert_eq!(ctx.context().items.len(), 5);
assert_eq!(ctx.context().items[0], ("".to_string(), "".to_string()));
assert_eq!(
ctx.context().items[1],
("empty_value".to_string(), "".to_string())
);
assert_eq!(
ctx.context().items[2],
("".to_string(), "empty_key".to_string())
);
assert_eq!(
ctx.context().items[3],
("special_chars".to_string(), "@#$%^&*()".to_string())
);
assert_eq!(
ctx.context().items[4],
("unicode".to_string(), "测试中文字符".to_string())
);
}
#[test]
fn test_context_take_multiple_calls() {
let mut ctx = OperationContext::new();
ctx.record("key1", "value1");
ctx.record("key2", "value2");
ctx.record("key1", "new_value1"); ctx.record("key3", &PathBuf::from("/path/file.txt"));
ctx.record("key2", &PathBuf::from("/path/file2.txt"));
assert_eq!(ctx.context().items.len(), 5);
assert_eq!(
ctx.context().items[0],
("key1".to_string(), "value1".to_string())
);
assert_eq!(
ctx.context().items[1],
("key2".to_string(), "value2".to_string())
);
assert_eq!(
ctx.context().items[2],
("key1".to_string(), "new_value1".to_string())
);
assert_eq!(ctx.context().items[3].0, "key3");
assert!(ctx.context().items[3].1.contains("/path/file.txt"));
assert_eq!(ctx.context().items[4].0, "key2");
assert!(ctx.context().items[4].1.contains("/path/file2.txt"));
}
#[test]
fn test_context_take_with_existing_context() {
let mut ctx = OperationContext::from(("existing_key", "existing_value"));
ctx.record("new_key1", "new_value1");
ctx.record("new_key2", &PathBuf::from("/new/path.txt"));
assert_eq!(ctx.context().items.len(), 3);
assert_eq!(
ctx.context().items[0],
("existing_key".to_string(), "existing_value".to_string())
);
assert_eq!(
ctx.context().items[1],
("new_key1".to_string(), "new_value1".to_string())
);
assert_eq!(ctx.context().items[2].0, "new_key2");
assert!(ctx.context().items[2].1.contains("/new/path.txt"));
}
}