1#[cfg(all(feature = "log", not(feature = "tracing")))]
2use log::{debug, error, info, trace, warn};
3use std::{
4 fmt::Display,
5 ops::{Deref, DerefMut},
6 path::{Path, PathBuf},
7};
8
9use super::metadata::{ErrorMetadata, MetadataValue};
10#[derive(Debug, Clone, PartialEq, Default)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum OperationResult {
13 Suc,
14 #[default]
15 Fail,
16 Cancel,
17}
18
19const DEFAULT_MOD_PATH: &str = module_path!();
21
22#[macro_export]
24macro_rules! op_context {
25 ($target:expr) => {
26 $crate::OperationContext::want($target).with_mod_path(module_path!())
27 };
28}
29
30#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct OperationContext {
33 context: CallContext,
34 result: OperationResult,
35 exit_log: bool,
36 mod_path: String,
37 target: Option<String>,
38 #[cfg_attr(feature = "serde", serde(default))]
39 path: Vec<String>,
40 #[cfg_attr(feature = "serde", serde(default))]
41 #[cfg_attr(
42 feature = "serde",
43 serde(skip_serializing_if = "ErrorMetadata::is_empty")
44 )]
45 metadata: ErrorMetadata,
46}
47impl Default for OperationContext {
48 fn default() -> Self {
49 Self {
50 context: CallContext::default(),
51 target: None,
52 path: Vec::new(),
53 result: OperationResult::Fail,
54 exit_log: false,
55 mod_path: DEFAULT_MOD_PATH.into(),
56 metadata: ErrorMetadata::default(),
57 }
58 }
59}
60pub type WithContext = OperationContext;
61impl From<CallContext> for OperationContext {
62 fn from(value: CallContext) -> Self {
63 OperationContext {
64 context: value,
65 result: OperationResult::Fail,
66 target: None,
67 path: Vec::new(),
68 exit_log: false,
69 mod_path: DEFAULT_MOD_PATH.into(),
70 metadata: ErrorMetadata::default(),
71 }
72 }
73}
74
75impl Drop for OperationContext {
76 fn drop(&mut self) {
77 if !self.exit_log {
78 return;
79 }
80
81 #[cfg(feature = "tracing")]
82 {
83 let ctx = self.format_context();
84 match self.result() {
85 OperationResult::Suc => {
86 tracing::info!(
87 target: "domain",
88 mod_path = %self.mod_path,
89 "suc! {ctx}"
90 )
91 }
92 OperationResult::Fail => {
93 tracing::error!(
94 target: "domain",
95 mod_path = %self.mod_path,
96 "fail! {ctx}"
97 )
98 }
99 OperationResult::Cancel => {
100 tracing::warn!(
101 target: "domain",
102 mod_path = %self.mod_path,
103 "cancel! {ctx}"
104 )
105 }
106 }
107 }
108
109 #[cfg(all(feature = "log", not(feature = "tracing")))]
110 {
111 match self.result() {
112 OperationResult::Suc => {
113 info!(target: self.mod_path.as_str(), "suc! {}", self.format_context());
114 }
115 OperationResult::Fail => {
116 error!(target: self.mod_path.as_str(), "fail! {}", self.format_context());
117 }
118 OperationResult::Cancel => {
119 warn!(target: self.mod_path.as_str(), "cancel! {}", self.format_context());
120 }
121 }
122 }
123 }
124}
125
126impl Display for OperationContext {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 if let Some(target) = &self.target {
129 writeln!(f, "want: {target}")?;
130 }
131 if let Some(path) = self.path_string() {
132 if self.target.as_deref() != Some(path.as_str()) {
133 writeln!(f, "path: {path}")?;
134 }
135 }
136 for (i, (k, v)) in self.context().items.iter().enumerate() {
137 writeln!(f, "{}. {k}: {v} ", i + 1)?;
138 }
139 Ok(())
140 }
141}
142pub trait ContextRecord<S1, S2> {
143 fn record(&mut self, key: S1, val: S2);
144}
145
146impl<S1> ContextRecord<S1, String> for OperationContext
147where
148 S1: Into<String>,
149{
150 fn record(&mut self, key: S1, val: String) {
151 self.context.items.push((key.into(), val));
152 }
153}
154
155impl<S1> ContextRecord<S1, &str> for OperationContext
156where
157 S1: Into<String>,
158{
159 fn record(&mut self, key: S1, val: &str) {
160 self.context.items.push((key.into(), val.into()));
161 }
162}
163
164impl<S1> ContextRecord<S1, &PathBuf> for OperationContext
167where
168 S1: Into<String>,
169{
170 fn record(&mut self, key: S1, val: &PathBuf) {
171 self.context
172 .items
173 .push((key.into(), format!("{}", val.display())));
174 }
175}
176impl<S1> ContextRecord<S1, &Path> for OperationContext
177where
178 S1: Into<String>,
179{
180 fn record(&mut self, key: S1, val: &Path) {
181 self.context
182 .items
183 .push((key.into(), format!("{}", val.display())));
184 }
185}
186
187impl OperationContext {
188 pub fn context(&self) -> &CallContext {
189 &self.context
190 }
191
192 pub fn result(&self) -> &OperationResult {
193 &self.result
194 }
195
196 pub fn exit_log(&self) -> &bool {
197 &self.exit_log
198 }
199
200 pub fn mod_path(&self) -> &String {
201 &self.mod_path
202 }
203
204 pub fn target(&self) -> &Option<String> {
205 &self.target
206 }
207
208 pub fn path(&self) -> &[String] {
209 &self.path
210 }
211
212 pub fn metadata(&self) -> &ErrorMetadata {
213 &self.metadata
214 }
215
216 pub fn new() -> Self {
217 Self {
218 target: None,
219 path: Vec::new(),
220 context: CallContext::default(),
221 result: OperationResult::Fail,
222 exit_log: false,
223 mod_path: DEFAULT_MOD_PATH.into(),
224 metadata: ErrorMetadata::default(),
225 }
226 }
227 pub fn want<S: Into<String>>(target: S) -> Self {
228 let target = target.into();
229 Self {
230 target: Some(target.clone()),
231 path: vec![target],
232 context: CallContext::default(),
233 result: OperationResult::Fail,
234 exit_log: false,
235 mod_path: DEFAULT_MOD_PATH.into(),
236 metadata: ErrorMetadata::default(),
237 }
238 }
239 #[deprecated(since = "0.5.4", note = "use with_auto_log")]
240 pub fn with_exit_log(mut self) -> Self {
241 self.exit_log = true;
242 self
243 }
244 pub fn with_auto_log(mut self) -> Self {
245 self.exit_log = true;
246 self
247 }
248 pub fn with_mod_path<S: Into<String>>(mut self, path: S) -> Self {
249 self.mod_path = path.into();
250 self
251 }
252 #[deprecated(since = "0.5.4", note = "use record")]
253 pub fn with<S1: Into<String>, S2: Into<String>>(&mut self, key: S1, val: S2) {
254 self.context.items.push((key.into(), val.into()));
255 }
256
257 #[deprecated(since = "0.5.4", note = "use record")]
258 pub fn with_path<S1: Into<String>, S2: Into<PathBuf>>(&mut self, key: S1, val: S2) {
259 self.context
260 .items
261 .push((key.into(), format!("{}", val.into().display())));
262 }
263
264 pub fn with_want<S: Into<String>>(&mut self, target: S) {
265 let target = target.into();
266 if target.is_empty() {
267 return;
268 }
269
270 if let Some(root) = &self.target {
271 if self.path.is_empty() {
272 self.path.push(root.clone());
273 }
274 if self.path.last() != Some(&target) {
275 self.path.push(target);
276 }
277 } else {
278 self.target = Some(target.clone());
279 self.path.push(target);
280 }
281 }
282 pub fn set_target<S: Into<String>>(&mut self, target: S) {
284 self.with_want(target)
285 }
286
287 pub fn path_string(&self) -> Option<String> {
288 if self.path.is_empty() {
289 None
290 } else {
291 Some(self.path.join(" / "))
292 }
293 }
294
295 pub fn record_meta<K, V>(&mut self, key: K, value: V)
296 where
297 K: Into<String>,
298 V: Into<MetadataValue>,
299 {
300 self.metadata.insert(key, value);
301 }
302
303 pub fn with_meta<K, V>(mut self, key: K, value: V) -> Self
304 where
305 K: Into<String>,
306 V: Into<MetadataValue>,
307 {
308 self.record_meta(key, value);
309 self
310 }
311
312 pub fn mark_suc(&mut self) {
313 self.result = OperationResult::Suc;
314 }
315 pub fn mark_cancel(&mut self) {
316 self.result = OperationResult::Cancel;
317 }
318
319 #[cfg_attr(not(any(feature = "log", feature = "tracing")), allow(dead_code))]
321 fn format_context(&self) -> String {
322 let want = self.target.clone().unwrap_or_default();
323 let path = self.path_string().unwrap_or_default();
324 let head = match (want.is_empty(), path.is_empty() || path == want) {
325 (true, true) => String::new(),
326 (false, true) => format!("want={want}"),
327 (false, false) => format!("want={want} path={path}"),
328 (true, false) => format!("path={path}"),
329 };
330 if self.context.items.is_empty() {
331 return head;
332 }
333 if head.is_empty() {
334 let body = self.context.to_string();
335 body.strip_prefix('\n').unwrap_or(&body).to_string()
336 } else {
337 format!("{head}: {}", self.context)
338 }
339 }
340
341 pub fn scope(&mut self) -> OperationScope<'_> {
343 OperationScope {
344 ctx: self,
345 mark_success: false,
346 }
347 }
348
349 pub fn scoped_success(&mut self) -> OperationScope<'_> {
351 OperationScope {
352 ctx: self,
353 mark_success: true,
354 }
355 }
356
357 #[cfg(feature = "tracing")]
360 pub fn info<S: AsRef<str>>(&self, message: S) {
361 tracing::info!(
362 target: "domain",
363 mod_path = %self.mod_path,
364 "{}: {}",
365 self.format_context(),
366 message.as_ref()
367 );
368 }
369 #[cfg(all(feature = "log", not(feature = "tracing")))]
370 pub fn info<S: AsRef<str>>(&self, message: S) {
371 info!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
372 }
373 #[cfg(not(any(feature = "log", feature = "tracing")))]
374 pub fn info<S: AsRef<str>>(&self, _message: S) {}
375
376 #[cfg(feature = "tracing")]
377 pub fn debug<S: AsRef<str>>(&self, message: S) {
378 tracing::debug!(
379 target: "domain",
380 mod_path = %self.mod_path,
381 "{}: {}",
382 self.format_context(),
383 message.as_ref()
384 );
385 }
386 #[cfg(all(feature = "log", not(feature = "tracing")))]
387 pub fn debug<S: AsRef<str>>(&self, message: S) {
388 debug!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
389 }
390 #[cfg(not(any(feature = "log", feature = "tracing")))]
391 pub fn debug<S: AsRef<str>>(&self, _message: S) {}
392
393 #[cfg(feature = "tracing")]
394 pub fn warn<S: AsRef<str>>(&self, message: S) {
395 tracing::warn!(
396 target: "domain",
397 mod_path = %self.mod_path,
398 "{}: {}",
399 self.format_context(),
400 message.as_ref()
401 );
402 }
403 #[cfg(all(feature = "log", not(feature = "tracing")))]
404 pub fn warn<S: AsRef<str>>(&self, message: S) {
405 warn!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
406 }
407 #[cfg(not(any(feature = "log", feature = "tracing")))]
408 pub fn warn<S: AsRef<str>>(&self, _message: S) {}
409
410 #[cfg(feature = "tracing")]
411 pub fn error<S: AsRef<str>>(&self, message: S) {
412 tracing::error!(
413 target: "domain",
414 mod_path = %self.mod_path,
415 "{}: {}",
416 self.format_context(),
417 message.as_ref()
418 );
419 }
420 #[cfg(all(feature = "log", not(feature = "tracing")))]
421 pub fn error<S: AsRef<str>>(&self, message: S) {
422 error!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
423 }
424 #[cfg(not(any(feature = "log", feature = "tracing")))]
425 pub fn error<S: AsRef<str>>(&self, _message: S) {}
426
427 #[cfg(feature = "tracing")]
428 pub fn trace<S: AsRef<str>>(&self, message: S) {
429 tracing::trace!(
430 target: "domain",
431 mod_path = %self.mod_path,
432 "{}: {}",
433 self.format_context(),
434 message.as_ref()
435 );
436 }
437 #[cfg(all(feature = "log", not(feature = "tracing")))]
438 pub fn trace<S: AsRef<str>>(&self, message: S) {
439 trace!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
440 }
441 #[cfg(not(any(feature = "log", feature = "tracing")))]
442 pub fn trace<S: AsRef<str>>(&self, _message: S) {}
443
444 pub fn log_info<S: AsRef<str>>(&self, message: S) {
446 self.info(message)
447 }
448 pub fn log_debug<S: AsRef<str>>(&self, message: S) {
449 self.debug(message)
450 }
451 pub fn log_warn<S: AsRef<str>>(&self, message: S) {
452 self.warn(message)
453 }
454 pub fn log_error<S: AsRef<str>>(&self, message: S) {
455 self.error(message)
456 }
457 pub fn log_trace<S: AsRef<str>>(&self, message: S) {
458 self.trace(message)
459 }
460
461 pub(crate) fn context_mut_for_report(&mut self) -> &mut CallContext {
462 &mut self.context
463 }
464
465 pub(crate) fn replace_target_for_report(&mut self, target: Option<String>) {
466 self.target = target;
467 }
468
469 pub(crate) fn replace_path_for_report(&mut self, path: Vec<String>) {
470 self.path = path;
471 }
472
473 pub(crate) fn replace_metadata_for_report(&mut self, metadata: ErrorMetadata) {
474 self.metadata = metadata;
475 }
476}
477
478pub struct OperationScope<'a> {
479 ctx: &'a mut OperationContext,
480 mark_success: bool,
481}
482
483impl<'a> OperationScope<'a> {
484 pub fn mark_success(&mut self) {
486 self.mark_success = true;
487 }
488
489 pub fn mark_failure(&mut self) {
491 self.mark_success = false;
492 }
493
494 pub fn cancel(&mut self) {
496 self.ctx.mark_cancel();
497 self.mark_success = false;
498 }
499}
500
501impl<'a> Deref for OperationScope<'a> {
502 type Target = OperationContext;
503
504 fn deref(&self) -> &Self::Target {
505 self.ctx
506 }
507}
508
509impl<'a> DerefMut for OperationScope<'a> {
510 fn deref_mut(&mut self) -> &mut Self::Target {
511 self.ctx
512 }
513}
514
515impl Drop for OperationScope<'_> {
516 fn drop(&mut self) {
517 if self.mark_success {
518 self.ctx.mark_suc();
519 }
520 }
521}
522
523impl From<String> for OperationContext {
524 fn from(value: String) -> Self {
525 Self {
526 target: None,
527 path: Vec::new(),
528 context: CallContext::from(("key", value.to_string())),
529 result: OperationResult::Fail,
530 exit_log: false,
531 mod_path: DEFAULT_MOD_PATH.into(),
532 metadata: ErrorMetadata::default(),
533 }
534 }
535}
536
537impl From<&PathBuf> for OperationContext {
538 fn from(value: &PathBuf) -> Self {
539 Self {
540 target: None,
541 path: Vec::new(),
542 context: CallContext::from(("path", format!("{}", value.display()))),
543 result: OperationResult::Fail,
544 exit_log: false,
545 mod_path: DEFAULT_MOD_PATH.into(),
546 metadata: ErrorMetadata::default(),
547 }
548 }
549}
550
551impl From<&Path> for OperationContext {
552 fn from(value: &Path) -> Self {
553 Self {
554 target: None,
555 path: Vec::new(),
556 context: CallContext::from(("path", format!("{}", value.display()))),
557 result: OperationResult::Fail,
558 exit_log: false,
559 mod_path: DEFAULT_MOD_PATH.into(),
560 metadata: ErrorMetadata::default(),
561 }
562 }
563}
564
565impl From<&str> for OperationContext {
566 fn from(value: &str) -> Self {
567 Self {
568 target: None,
569 path: Vec::new(),
570 context: CallContext::from(("key", value.to_string())),
571 result: OperationResult::Fail,
572 exit_log: false,
573 mod_path: DEFAULT_MOD_PATH.into(),
574 metadata: ErrorMetadata::default(),
575 }
576 }
577}
578
579impl From<(&str, &str)> for OperationContext {
580 fn from(value: (&str, &str)) -> Self {
581 Self {
582 target: None,
583 path: Vec::new(),
584 context: CallContext::from((value.0, value.1)),
585 result: OperationResult::Fail,
586 exit_log: false,
587 mod_path: DEFAULT_MOD_PATH.into(),
588 metadata: ErrorMetadata::default(),
589 }
590 }
591}
592
593impl From<(&str, String)> for OperationContext {
594 fn from(value: (&str, String)) -> Self {
595 Self {
596 target: None,
597 path: Vec::new(),
598 context: CallContext::from((value.0, value.1)),
599 result: OperationResult::Fail,
600 exit_log: false,
601 mod_path: DEFAULT_MOD_PATH.into(),
602 metadata: ErrorMetadata::default(),
603 }
604 }
605}
606trait NotAsRefStr: AsRef<Path> {}
608
609impl NotAsRefStr for PathBuf {}
611impl NotAsRefStr for Path {}
612impl<T: AsRef<Path> + ?Sized> NotAsRefStr for &T where T: NotAsRefStr {}
613
614impl<V: AsRef<Path>> From<(&str, V)> for OperationContext
615where
616 V: NotAsRefStr,
617{
618 fn from(value: (&str, V)) -> Self {
619 Self {
620 target: None,
621 path: Vec::new(),
622 context: CallContext {
623 items: vec![(
624 value.0.to_string(),
625 format!("{}", value.1.as_ref().display()),
626 )],
627 },
628 result: OperationResult::Fail,
629 exit_log: false,
630 mod_path: DEFAULT_MOD_PATH.into(),
631 metadata: ErrorMetadata::default(),
632 }
633 }
634}
635
636impl From<(String, String)> for OperationContext {
637 fn from(value: (String, String)) -> Self {
638 Self {
639 target: None,
640 path: Vec::new(),
641 context: CallContext::from((value.0, value.1)),
642 result: OperationResult::Fail,
643 exit_log: false,
644 mod_path: DEFAULT_MOD_PATH.into(),
645 metadata: ErrorMetadata::default(),
646 }
647 }
648}
649
650impl From<&OperationContext> for OperationContext {
651 fn from(value: &OperationContext) -> Self {
652 value.clone()
653 }
654}
655
656#[derive(Default, Debug, Clone, PartialEq)]
657#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
658pub struct CallContext {
659 pub items: Vec<(String, String)>,
660}
661
662impl<K: AsRef<str>, V: AsRef<str>> From<(K, V)> for CallContext {
663 fn from(value: (K, V)) -> Self {
664 Self {
665 items: vec![(value.0.as_ref().to_string(), value.1.as_ref().to_string())],
666 }
667 }
668}
669
670pub trait ContextAdd<T> {
671 fn add_context(&mut self, val: T);
672}
673
674impl<K: Into<String>> ContextAdd<(K, String)> for OperationContext {
675 fn add_context(&mut self, val: (K, String)) {
676 self.record(val.0.into(), val.1);
677 }
678}
679impl<K: Into<String>> ContextAdd<(K, &String)> for OperationContext {
680 fn add_context(&mut self, val: (K, &String)) {
681 self.record(val.0.into(), val.1.clone());
682 }
683}
684impl<K: Into<String>> ContextAdd<(K, &str)> for OperationContext {
685 fn add_context(&mut self, val: (K, &str)) {
686 self.record(val.0.into(), val.1.to_string());
687 }
688}
689
690impl<K: Into<String>> ContextAdd<(K, &PathBuf)> for OperationContext {
691 fn add_context(&mut self, val: (K, &PathBuf)) {
692 self.record(val.0.into(), format!("{}", val.1.display()));
693 }
694}
695impl<K: Into<String>> ContextAdd<(K, &Path)> for OperationContext {
696 fn add_context(&mut self, val: (K, &Path)) {
697 self.record(val.0.into(), format!("{}", val.1.display()));
698 }
699}
700
701impl Display for CallContext {
702 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703 if !self.items.is_empty() {
704 writeln!(f, "\ncall context:")?;
705 }
706 for (k, v) in &self.items {
707 writeln!(f, "\t{k} : {v}")?;
708 }
709 Ok(())
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use std::path::PathBuf;
717
718 #[test]
719 fn test_op_context_macro_sets_callsite_mod_path() {
720 let ctx = crate::op_context!("macro_target");
721 assert_eq!(*ctx.target(), Some("macro_target".to_string()));
722 assert_eq!(ctx.mod_path().as_str(), module_path!());
723 }
724
725 #[test]
726 fn test_withcontext_new() {
727 let ctx = OperationContext::new();
728 assert!(ctx.target.is_none());
729 assert_eq!(ctx.context().items.len(), 0);
730 }
731
732 #[test]
733 fn test_withcontext_want() {
734 let ctx = OperationContext::want("test_target");
735 assert_eq!(*ctx.target(), Some("test_target".to_string()));
736 assert_eq!(ctx.path(), &["test_target".to_string()]);
737 assert_eq!(ctx.context().items.len(), 0);
738 }
739
740 #[test]
741 fn test_withcontext_with() {
742 let mut ctx = OperationContext::new();
743 ctx.record("key1", "value1");
744 ctx.record("key2", "value2");
745
746 assert_eq!(ctx.context().items.len(), 2);
747 assert_eq!(
748 ctx.context().items[0],
749 ("key1".to_string(), "value1".to_string())
750 );
751 assert_eq!(
752 ctx.context().items[1],
753 ("key2".to_string(), "value2".to_string())
754 );
755 }
756
757 #[test]
758 fn test_withcontext_with_path() {
759 let mut ctx = OperationContext::new();
760 let path = PathBuf::from("/test/path");
761 ctx.record("file_path", &path);
762
763 assert_eq!(ctx.context().items.len(), 1);
764 assert!(ctx.context().items[0].1.contains("/test/path"));
765 }
766
767 #[test]
768 fn test_withcontext_with_want() {
769 let mut ctx = OperationContext::new();
770 ctx.with_want("new_target");
771
772 assert_eq!(*ctx.target(), Some("new_target".to_string()));
773 assert_eq!(ctx.path(), &["new_target".to_string()]);
774 }
775
776 #[test]
777 fn test_withcontext_with_want_appends_path() {
778 let mut ctx = OperationContext::want("place_order");
779 ctx.with_want("read_order_payload");
780 ctx.with_want("parse_order");
781
782 assert_eq!(*ctx.target(), Some("place_order".to_string()));
783 assert_eq!(
784 ctx.path(),
785 &[
786 "place_order".to_string(),
787 "read_order_payload".to_string(),
788 "parse_order".to_string()
789 ]
790 );
791 assert_eq!(
792 ctx.path_string().as_deref(),
793 Some("place_order / read_order_payload / parse_order")
794 );
795 }
796
797 #[test]
798 fn test_errcontext_from_string() {
799 let ctx = CallContext::from(("key".to_string(), "test_string".to_string()));
800 assert_eq!(ctx.items.len(), 1);
801 assert_eq!(ctx.items[0], ("key".to_string(), "test_string".to_string()));
802 }
803
804 #[test]
805 fn test_errcontext_from_str() {
806 let ctx = CallContext::from(("key", "test_str"));
807 assert_eq!(ctx.items.len(), 1);
808 assert_eq!(ctx.items[0], ("key".to_string(), "test_str".to_string()));
809 }
810
811 #[test]
812 fn test_errcontext_from_string_pair() {
813 let ctx = CallContext::from(("key1".to_string(), "value1".to_string()));
814 assert_eq!(ctx.items.len(), 1);
815 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
816 }
817
818 #[test]
819 fn test_errcontext_from_str_pair() {
820 let ctx = CallContext::from(("key1", "value1"));
821 assert_eq!(ctx.items.len(), 1);
822 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
823 }
824
825 #[test]
826 fn test_errcontext_from_mixed_pair() {
827 let ctx = CallContext::from(("key1", "value1".to_string()));
828 assert_eq!(ctx.items.len(), 1);
829 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
830 }
831
832 #[test]
833 fn test_errcontext_default() {
834 let ctx = CallContext::default();
835 assert_eq!(ctx.items.len(), 0);
836 }
837
838 #[test]
839 fn test_errcontext_display_single() {
840 let ctx = CallContext::from(("key", "test"));
841 let display = format!("{ctx}");
842 assert!(display.contains("call context:"));
843 assert!(display.contains("key : test"));
844 }
845
846 #[test]
847 fn test_errcontext_display_multiple() {
848 let mut ctx = CallContext::default();
849 ctx.items.push(("key1".to_string(), "value1".to_string()));
850 ctx.items.push(("key2".to_string(), "value2".to_string()));
851 let display = format!("{ctx}");
852 assert!(display.contains("call context:"));
853 assert!(display.contains("key1 : value1"));
854 assert!(display.contains("key2 : value2"));
855 }
856
857 #[test]
858 fn test_errcontext_display_empty() {
859 let ctx = CallContext::default();
860 let display = format!("{ctx}");
861 assert_eq!(display, "");
862 }
863
864 #[test]
865 fn test_withcontext_from_string() {
866 let ctx = OperationContext::from("test_string".to_string());
867 assert!(ctx.target.is_none());
868 assert_eq!(ctx.context().items.len(), 1);
869 assert_eq!(
870 ctx.context().items[0],
871 ("key".to_string(), "test_string".to_string())
872 );
873 }
874
875 #[test]
876 fn test_withcontext_from_str() {
877 let ctx = OperationContext::from("test_str".to_string());
878 assert!(ctx.target.is_none());
879 assert_eq!(ctx.context().items.len(), 1);
880 assert_eq!(
881 ctx.context().items[0],
882 ("key".to_string(), "test_str".to_string())
883 );
884 }
885
886 #[test]
887 fn test_withcontext_from_pathbuf() {
888 let path = PathBuf::from("/test/path");
889 let ctx = OperationContext::from(&path);
890 assert!(ctx.target.is_none());
891 assert_eq!(ctx.context().items.len(), 1);
892 assert!(ctx.context().items[0].1.contains("/test/path"));
893 }
894
895 #[test]
896 fn test_withcontext_from_path() {
897 let path = "/test/path";
898 let ctx = OperationContext::from(path);
899 assert!(ctx.target.is_none());
900 assert_eq!(ctx.context().items.len(), 1);
901 assert!(ctx.context().items[0].1.contains("/test/path"));
902 }
903
904 #[test]
905 fn test_withcontext_from_string_pair() {
906 let ctx = OperationContext::from(("key1".to_string(), "value1".to_string()));
907 assert!(ctx.target.is_none());
908 assert_eq!(ctx.context().items.len(), 1);
909 assert_eq!(
910 ctx.context().items[0],
911 ("key1".to_string(), "value1".to_string())
912 );
913 }
914
915 #[test]
916 fn test_withcontext_from_str_pair() {
917 let ctx = OperationContext::from(("key1", "value1"));
918 assert!(ctx.target.is_none());
919 assert_eq!(ctx.context().items.len(), 1);
920 assert_eq!(
921 ctx.context().items[0],
922 ("key1".to_string(), "value1".to_string())
923 );
924 }
925
926 #[test]
927 fn test_withcontext_from_mixed_pair() {
928 let ctx = OperationContext::from(("key1", "value1".to_string()));
929 assert!(ctx.target.is_none());
930 assert_eq!(ctx.context().items.len(), 1);
931 assert_eq!(
932 ctx.context().items[0],
933 ("key1".to_string(), "value1".to_string())
934 );
935 }
936
937 #[test]
938 fn test_withcontext_from_path_pair() {
939 let path = PathBuf::from("/test/path");
940 let ctx = OperationContext::from(("file", path.to_string_lossy().as_ref()));
941 assert!(ctx.target.is_none());
942 assert_eq!(ctx.context().items.len(), 1);
943 assert!(ctx.context().items[0].0.contains("file"));
944 assert!(ctx.context().items[0].1.contains("/test/path"));
945 }
946
947 #[test]
948 fn test_withcontext_display_with_target() {
949 let mut ctx = OperationContext::want("test_target");
950 ctx.record("key1", "value1");
951 let display = format!("{ctx}");
952 assert!(display.contains("want: test_target"));
953 assert!(display.contains("1. key1: value1"));
954 }
955
956 #[test]
957 fn test_withcontext_display_without_target() {
958 let mut ctx = OperationContext::new();
959 ctx.record("key1", "value1");
960 let display = format!("{ctx}");
961 assert!(!display.contains("target:"));
962 assert!(display.contains("1. key1: value1"));
963 }
964
965 #[test]
966 fn test_withcontext_from_errcontext() {
967 let err_ctx = CallContext::from(("key1", "value1"));
968 let ctx = OperationContext::from(err_ctx);
969 assert!(ctx.target.is_none());
970 assert_eq!(ctx.context().items.len(), 1);
971 assert_eq!(
972 ctx.context().items[0],
973 ("key1".to_string(), "value1".to_string())
974 );
975 }
976
977 #[test]
978 fn test_withcontext_from_withcontext() {
979 let mut ctx1 = OperationContext::want("target1");
980 ctx1.record("key1", "value1");
981 ctx1.with_want("step1");
982 let ctx2 = OperationContext::from(&ctx1);
983 assert_eq!(*ctx2.target(), Some("target1".to_string()));
984 assert_eq!(ctx2.path(), &["target1".to_string(), "step1".to_string()]);
985 assert_eq!(ctx2.context().items.len(), 1);
986 assert_eq!(
987 ctx2.context().items[0],
988 ("key1".to_string(), "value1".to_string())
989 );
990 }
991
992 #[test]
993 fn test_withcontext_from_str_path_pair() {
994 let path = PathBuf::from("/test/path");
995 let ctx = OperationContext::from(("file", &path));
996 assert_eq!(ctx.context().items.len(), 1);
997 assert_eq!(ctx.context().items[0].0, "file");
998 assert!(ctx.context().items[0].1.contains("/test/path"));
999 }
1000
1001 #[test]
1002 fn test_withcontext_from_str_pathbuf_pair() {
1003 let path = PathBuf::from("/test/pathbuf");
1004 let ctx = OperationContext::from(("file", path));
1005 assert_eq!(ctx.context().items.len(), 1);
1006 assert_eq!(ctx.context().items[0].0, "file");
1007 assert!(ctx.context().items[0].1.contains("/test/pathbuf"));
1008 }
1009
1010 #[test]
1014 fn test_withcontext_edge_cases() {
1015 let ctx1 = OperationContext::from("".to_string());
1016 assert_eq!(ctx1.context().items.len(), 1);
1017 assert_eq!(ctx1.context().items[0], ("key".to_string(), "".to_string()));
1018
1019 let ctx2 = OperationContext::from(("".to_string(), "".to_string()));
1020 assert_eq!(ctx2.context().items.len(), 1);
1021 assert_eq!(ctx2.context().items[0], ("".to_string(), "".to_string()));
1022 }
1023
1024 #[test]
1025 fn test_errcontext_equality() {
1026 let ctx1 = CallContext::from(("key1", "value1"));
1027 let ctx2 = CallContext::from(("key1", "value1"));
1028 let ctx3 = CallContext::from(("key1", "value2"));
1029
1030 assert_eq!(ctx1, ctx2);
1031 assert_ne!(ctx1, ctx3);
1032 }
1033
1034 #[test]
1035 fn test_withcontext_equality() {
1036 let ctx1 = OperationContext::from(("key1", "value1"));
1037 let ctx2 = OperationContext::from(("key1", "value1"));
1038 let ctx3 = OperationContext::from(("key1", "value2"));
1039
1040 assert_eq!(ctx1, ctx2);
1041 assert_ne!(ctx1, ctx3);
1042 }
1043
1044 #[test]
1045 fn test_withcontext_clone() {
1046 let mut ctx = OperationContext::want("target");
1047 ctx.record("key", "value");
1048
1049 let cloned = ctx.clone();
1050 assert_eq!(ctx.target(), cloned.target());
1051 assert_eq!(ctx.context().items.len(), cloned.context().items.len());
1052 assert_eq!(ctx.context().items[0], cloned.context().items[0]);
1053 }
1054
1055 #[test]
1056 fn test_withcontext_with_types() {
1057 let mut ctx = OperationContext::new();
1058
1059 ctx.record("string_key", "string_value");
1061 ctx.record("string_key", 42.to_string()); ctx.record("bool_key", true.to_string()); assert_eq!(ctx.context().items.len(), 3);
1065
1066 assert_eq!(
1068 ctx.context().items[2],
1069 ("bool_key".to_string(), "true".to_string())
1070 );
1071 }
1072
1073 #[test]
1074 fn test_mark_suc() {
1075 let mut ctx = OperationContext::new();
1076 assert!(ctx.result == OperationResult::Fail);
1077
1078 ctx.mark_suc();
1079 assert!(ctx.result == OperationResult::Suc);
1080 }
1081
1082 #[test]
1083 fn test_with_exit_log() {
1084 let ctx = OperationContext::new().with_auto_log();
1085 assert!(ctx.exit_log);
1086
1087 let ctx2 = OperationContext::want("test").with_auto_log();
1088 assert!(ctx2.exit_log);
1089 assert_eq!(*ctx2.target(), Some("test".to_string()));
1090 }
1091
1092 #[test]
1093 fn test_scope_marks_success() {
1094 let mut ctx = OperationContext::want("scope_success");
1095 {
1096 let _scope = ctx.scoped_success();
1097 }
1098 assert!(matches!(ctx.result(), OperationResult::Suc));
1099 }
1100
1101 #[test]
1102 fn test_scope_preserves_failure() {
1103 let mut ctx = OperationContext::want("scope_fail");
1104 {
1105 let mut scope = ctx.scoped_success();
1106 scope.mark_failure();
1107 }
1108 assert!(matches!(ctx.result(), OperationResult::Fail));
1109 }
1110
1111 #[test]
1112 fn test_scope_cancel() {
1113 let mut ctx = OperationContext::want("scope_cancel");
1114 {
1115 let mut scope = ctx.scoped_success();
1116 scope.cancel();
1117 }
1118 assert!(matches!(ctx.result(), OperationResult::Cancel));
1119 }
1120
1121 #[test]
1122 fn test_format_context_with_target() {
1123 let mut ctx = OperationContext::want("test_target");
1124 ctx.record("key1", "value1");
1125
1126 let formatted = ctx.format_context();
1127 assert_eq!(
1128 formatted,
1129 "want=test_target: \ncall context:\n\tkey1 : value1\n"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_format_context_without_target() {
1135 let mut ctx = OperationContext::new();
1136 ctx.record("key1", "value1");
1137
1138 let formatted = ctx.format_context();
1139 assert_eq!(formatted, "call context:\n\tkey1 : value1\n");
1140 }
1141
1142 #[test]
1143 fn test_format_context_empty() {
1144 let ctx = OperationContext::new();
1145 let formatted = ctx.format_context();
1146 assert_eq!(formatted, "");
1147 }
1148
1149 #[test]
1150 fn test_format_context_with_target_only() {
1151 let ctx = OperationContext::want("test_target");
1152 let formatted = ctx.format_context();
1153 assert_eq!(formatted, "want=test_target");
1154 }
1155
1156 #[test]
1157 fn test_format_context_with_path() {
1158 let mut ctx = OperationContext::want("place_order");
1159 ctx.with_want("read_order_payload");
1160 ctx.record("order_id", "42");
1161
1162 let formatted = ctx.format_context();
1163 assert_eq!(
1164 formatted,
1165 "want=place_order path=place_order / read_order_payload: \ncall context:\n\torder_id : 42\n"
1166 );
1167 }
1168
1169 #[test]
1170 fn test_logging_methods() {
1171 let ctx = OperationContext::want("test_target");
1172
1173 ctx.info("info message");
1175 ctx.debug("debug message");
1176 ctx.warn("warn message");
1177 ctx.error("error message");
1178 ctx.trace("trace message");
1179 }
1180
1181 #[test]
1182 fn test_logging_methods_with_empty_context() {
1183 let ctx = OperationContext::new();
1184
1185 ctx.info("info message");
1187 ctx.debug("debug message");
1188 ctx.warn("warn message");
1189 ctx.error("error message");
1190 ctx.trace("trace message");
1191 }
1192
1193 #[test]
1194 fn test_context_add_trait() {
1195 let mut ctx = OperationContext::new();
1196
1197 ctx.add_context(("key1", "value1"));
1199 ctx.add_context(("key2", "value2"));
1200
1201 assert_eq!(ctx.context().items.len(), 2);
1202 assert_eq!(
1203 ctx.context().items[0],
1204 ("key1".to_string(), "value1".to_string())
1205 );
1206 assert_eq!(
1207 ctx.context().items[1],
1208 ("key2".to_string(), "value2".to_string())
1209 );
1210 }
1211
1212 #[test]
1213 fn test_drop_trait_with_success() {
1214 {
1215 let mut ctx = OperationContext::want("test_drop").with_auto_log();
1216 ctx.record("operation", "test");
1217 ctx.mark_suc(); }
1220 }
1223
1224 #[test]
1225 fn test_drop_trait_with_failure() {
1226 {
1227 let mut ctx = OperationContext::want("test_drop_fail").with_auto_log();
1228 ctx.record("operation", "test_fail");
1229 }
1232 }
1235
1236 #[test]
1237 fn test_drop_trait_without_exit_log() {
1238 {
1239 let mut ctx = OperationContext::want("test_no_log");
1240 ctx.record("operation", "no_log");
1241 ctx.mark_suc();
1242 }
1245 }
1247
1248 #[test]
1249 fn test_complex_context_scenario() {
1250 let mut ctx = OperationContext::want("user_registration").with_auto_log();
1252
1253 ctx.record("user_id", "12345");
1255 ctx.record("email", "test@example.com");
1256 ctx.record("role", "user");
1257
1258 ctx.info("开始用户注册流程");
1260 ctx.debug("验证用户输入");
1261 ctx.warn("检测到潜在的安全风险");
1262
1263 ctx.mark_suc();
1265 ctx.info("用户注册成功");
1266
1267 assert!(ctx.result == OperationResult::Suc);
1269 assert!(ctx.exit_log);
1270 assert_eq!(*ctx.target(), Some("user_registration".to_string()));
1271 assert_eq!(ctx.context().items.len(), 3);
1272
1273 let formatted = ctx.format_context();
1275 assert!(formatted.contains("user_registration"));
1276 assert!(formatted.contains("user_id"));
1277 assert!(formatted.contains("email"));
1278 assert!(formatted.contains("role"));
1279 }
1280
1281 #[cfg(feature = "serde")]
1282 #[test]
1283 fn test_context_serialization() {
1284 let mut ctx = OperationContext::want("serialization_test");
1285 ctx.with_want("inner_step");
1286 ctx.record("key1", "value1");
1287 ctx.record("key2", "value2");
1288
1289 let serialized = serde_json::to_string(&ctx).expect("序列化失败");
1291 assert!(serialized.contains("serialization_test"));
1292 assert!(serialized.contains("inner_step"));
1293 assert!(serialized.contains("key1"));
1294 assert!(serialized.contains("value1"));
1295
1296 let deserialized: OperationContext =
1298 serde_json::from_str(&serialized).expect("反序列化失败");
1299 assert_eq!(ctx, deserialized);
1300 }
1301
1302 #[test]
1303 fn test_context_with_special_characters() {
1304 let mut ctx = OperationContext::new();
1305
1306 ctx.record("key_with_spaces", "value with spaces");
1308 ctx.record("key_with_unicode", "值包含中文");
1309 ctx.record("key_with_symbols", "value@#$%^&*()");
1310
1311 assert_eq!(ctx.context().items.len(), 3);
1312 assert_eq!(
1313 ctx.context().items[0],
1314 (
1315 "key_with_spaces".to_string(),
1316 "value with spaces".to_string()
1317 )
1318 );
1319 assert_eq!(
1320 ctx.context().items[1],
1321 ("key_with_unicode".to_string(), "值包含中文".to_string())
1322 );
1323 assert_eq!(
1324 ctx.context().items[2],
1325 ("key_with_symbols".to_string(), "value@#$%^&*()".to_string())
1326 );
1327
1328 let display = format!("{ctx}");
1330 assert!(display.contains("key_with_spaces"));
1331 assert!(display.contains("值包含中文"));
1332 assert!(display.contains("value@#$%^&*()"));
1333 }
1334
1335 #[test]
1336 fn test_context_builder_pattern() {
1337 let ctx = OperationContext::want("builder_test").with_auto_log();
1339
1340 assert_eq!(*ctx.target(), Some("builder_test".to_string()));
1341 assert_eq!(ctx.path(), &["builder_test".to_string()]);
1342 assert!(ctx.exit_log);
1343 }
1344
1345 #[test]
1346 fn test_context_multiple_with_calls() {
1347 let mut ctx = OperationContext::new();
1348
1349 ctx.record("key1", "value1");
1351 ctx.record("key2", "value2");
1352 ctx.record("key3", "value3");
1353 ctx.record("key1", "new_value1"); assert_eq!(ctx.context().items.len(), 4);
1357 assert_eq!(
1358 ctx.context().items[0],
1359 ("key1".to_string(), "value1".to_string())
1360 );
1361 assert_eq!(
1362 ctx.context().items[3],
1363 ("key1".to_string(), "new_value1".to_string())
1364 );
1365 }
1366
1367 #[test]
1368 fn test_context_from_various_types() {
1369 let ctx1 = OperationContext::from("simple_string");
1371 assert_eq!(
1372 ctx1.context().items[0],
1373 ("key".to_string(), "simple_string".to_string())
1374 );
1375
1376 let ctx2 = OperationContext::from(("custom_key", "custom_value"));
1377 assert_eq!(
1378 ctx2.context().items[0],
1379 ("custom_key".to_string(), "custom_value".to_string())
1380 );
1381
1382 let path = PathBuf::from("/test/path/file.txt");
1383 let ctx3 = OperationContext::from(&path);
1384 assert!(ctx3.context().items[0].0.contains("path"));
1385 assert!(ctx3.context().items[0].1.contains("/test/path/file.txt"));
1386 }
1387
1388 #[test]
1390 fn test_context_take_with_string_types() {
1391 let mut ctx = OperationContext::new();
1392
1393 ctx.record("string_key", "string_value");
1395 ctx.record("string_key2", String::from("string_value2"));
1396 ctx.record(String::from("string_key3"), "string_value3");
1397 ctx.record(String::from("string_key4"), String::from("string_value4"));
1398
1399 assert_eq!(ctx.context().items.len(), 4);
1400 assert_eq!(
1401 ctx.context().items[0],
1402 ("string_key".to_string(), "string_value".to_string())
1403 );
1404 assert_eq!(
1405 ctx.context().items[1],
1406 ("string_key2".to_string(), "string_value2".to_string())
1407 );
1408 assert_eq!(
1409 ctx.context().items[2],
1410 ("string_key3".to_string(), "string_value3".to_string())
1411 );
1412 assert_eq!(
1413 ctx.context().items[3],
1414 ("string_key4".to_string(), "string_value4".to_string())
1415 );
1416 }
1417
1418 #[test]
1419 fn test_context_take_with_numeric_types() {
1420 let mut ctx = OperationContext::new();
1421
1422 ctx.record("int_key", 42.to_string());
1424 ctx.record("float_key", 3.24.to_string());
1425 ctx.record("bool_key", true.to_string());
1426
1427 assert_eq!(ctx.context().items.len(), 3);
1428 assert_eq!(
1429 ctx.context().items[0],
1430 ("int_key".to_string(), "42".to_string())
1431 );
1432 assert_eq!(
1433 ctx.context().items[1],
1434 ("float_key".to_string(), "3.24".to_string())
1435 );
1436 assert_eq!(
1437 ctx.context().items[2],
1438 ("bool_key".to_string(), "true".to_string())
1439 );
1440 }
1441
1442 #[test]
1443 fn test_context_take_with_path_context() {
1444 let mut ctx = OperationContext::new();
1445
1446 let path1 = PathBuf::from("/test/path1.txt");
1448 let path2 = Path::new("/test/path2.txt");
1449
1450 ctx.record("file1", &path1);
1451 ctx.record("file2", path2);
1452
1453 assert_eq!(ctx.context().items.len(), 2);
1454 assert_eq!(ctx.context().items[0].0, "file1");
1455 assert!(ctx.context().items[0].1.contains("/test/path1.txt"));
1456 assert_eq!(ctx.context().items[1].0, "file2");
1457 assert!(ctx.context().items[1].1.contains("/test/path2.txt"));
1458 }
1459
1460 #[test]
1461 fn test_context_take_mixed_types() {
1462 let mut ctx = OperationContext::new();
1463
1464 ctx.record("name", "test_user");
1466 ctx.record("age", 25.to_string());
1467 ctx.record("config_file", &PathBuf::from("/etc/config.toml"));
1468 ctx.record("status", "active");
1469
1470 assert_eq!(ctx.context().items.len(), 4);
1471 assert_eq!(
1472 ctx.context().items[0],
1473 ("name".to_string(), "test_user".to_string())
1474 );
1475 assert_eq!(
1476 ctx.context().items[1],
1477 ("age".to_string(), "25".to_string())
1478 );
1479 assert_eq!(ctx.context().items[2].0, "config_file");
1480 assert!(ctx.context().items[2].1.contains("/etc/config.toml"));
1481 assert_eq!(
1482 ctx.context().items[3],
1483 ("status".to_string(), "active".to_string())
1484 );
1485 }
1486
1487 #[test]
1488 fn test_context_take_edge_cases() {
1489 let mut ctx = OperationContext::new();
1490
1491 ctx.record("", ""); ctx.record("empty_value", ""); ctx.record("", "empty_key"); ctx.record("special_chars", "@#$%^&*()"); ctx.record("unicode", "测试中文字符"); assert_eq!(ctx.context().items.len(), 5);
1499 assert_eq!(ctx.context().items[0], ("".to_string(), "".to_string()));
1500 assert_eq!(
1501 ctx.context().items[1],
1502 ("empty_value".to_string(), "".to_string())
1503 );
1504 assert_eq!(
1505 ctx.context().items[2],
1506 ("".to_string(), "empty_key".to_string())
1507 );
1508 assert_eq!(
1509 ctx.context().items[3],
1510 ("special_chars".to_string(), "@#$%^&*()".to_string())
1511 );
1512 assert_eq!(
1513 ctx.context().items[4],
1514 ("unicode".to_string(), "测试中文字符".to_string())
1515 );
1516 }
1517
1518 #[test]
1519 fn test_context_take_multiple_calls() {
1520 let mut ctx = OperationContext::new();
1521
1522 ctx.record("key1", "value1");
1524 ctx.record("key2", "value2");
1525 ctx.record("key1", "new_value1"); ctx.record("key3", &PathBuf::from("/path/file.txt"));
1527 ctx.record("key2", &PathBuf::from("/path/file2.txt")); assert_eq!(ctx.context().items.len(), 5);
1531 assert_eq!(
1532 ctx.context().items[0],
1533 ("key1".to_string(), "value1".to_string())
1534 );
1535 assert_eq!(
1536 ctx.context().items[1],
1537 ("key2".to_string(), "value2".to_string())
1538 );
1539 assert_eq!(
1540 ctx.context().items[2],
1541 ("key1".to_string(), "new_value1".to_string())
1542 );
1543 assert_eq!(ctx.context().items[3].0, "key3");
1544 assert!(ctx.context().items[3].1.contains("/path/file.txt"));
1545 assert_eq!(ctx.context().items[4].0, "key2");
1546 assert!(ctx.context().items[4].1.contains("/path/file2.txt"));
1547 }
1548
1549 #[test]
1550 fn test_context_take_with_existing_context() {
1551 let mut ctx = OperationContext::from(("existing_key", "existing_value"));
1553
1554 ctx.record("new_key1", "new_value1");
1556 ctx.record("new_key2", &PathBuf::from("/new/path.txt"));
1557
1558 assert_eq!(ctx.context().items.len(), 3);
1559 assert_eq!(
1560 ctx.context().items[0],
1561 ("existing_key".to_string(), "existing_value".to_string())
1562 );
1563 assert_eq!(
1564 ctx.context().items[1],
1565 ("new_key1".to_string(), "new_value1".to_string())
1566 );
1567 assert_eq!(ctx.context().items[2].0, "new_key2");
1568 assert!(ctx.context().items[2].1.contains("/new/path.txt"));
1569 }
1570
1571 #[test]
1572 fn test_context_metadata_records_values() {
1573 let ctx = OperationContext::want("load")
1574 .with_meta("config.kind", "wpsrc")
1575 .with_meta("parse.line", 1u32)
1576 .with_meta("parse.strict", true);
1577
1578 assert_eq!(ctx.metadata().get_str("config.kind"), Some("wpsrc"));
1579 assert!(ctx.metadata().as_map().contains_key("parse.line"));
1580 assert!(ctx.metadata().as_map().contains_key("parse.strict"));
1581 }
1582
1583 #[test]
1584 fn test_context_metadata_duplicate_key_overwrites() {
1585 let ctx = OperationContext::new()
1586 .with_meta("config.kind", "sink_route")
1587 .with_meta("config.kind", "sink_defaults");
1588
1589 assert_eq!(ctx.metadata().get_str("config.kind"), Some("sink_defaults"));
1590 }
1591
1592 #[test]
1593 fn test_context_metadata_ignores_empty_key() {
1594 let result = std::panic::catch_unwind(|| OperationContext::new().with_meta("", "ignored"));
1595
1596 if cfg!(debug_assertions) {
1597 assert!(result.is_err());
1598 } else {
1599 let ctx = result.expect("release build should ignore empty metadata key");
1600 assert!(ctx.metadata().is_empty());
1601 }
1602 }
1603
1604 #[cfg(feature = "serde")]
1605 #[test]
1606 fn test_context_serialization_skips_empty_metadata_and_reads_missing_field() {
1607 let ctx = OperationContext::want("serialization_test");
1608
1609 let serialized = serde_json::to_value(&ctx).expect("序列化失败");
1610 assert!(!serialized
1611 .as_object()
1612 .expect("object")
1613 .contains_key("metadata"));
1614
1615 let deserialized: OperationContext =
1616 serde_json::from_value(serialized).expect("反序列化失败");
1617 assert!(deserialized.metadata().is_empty());
1618 }
1619
1620 #[cfg(feature = "serde")]
1621 #[test]
1622 fn test_context_serialization_preserves_metadata() {
1623 let ctx = OperationContext::want("serialization_test")
1624 .with_meta("config.kind", "sink_defaults")
1625 .with_meta("parse.line", 3u32);
1626
1627 let serialized = serde_json::to_value(&ctx).expect("序列化失败");
1628 assert_eq!(
1629 serialized["metadata"]["config.kind"],
1630 serde_json::Value::String("sink_defaults".to_string())
1631 );
1632 assert_eq!(serialized["metadata"]["parse.line"], serde_json::json!(3));
1633
1634 let deserialized: OperationContext =
1635 serde_json::from_value(serialized).expect("反序列化失败");
1636 assert_eq!(
1637 deserialized.metadata().get_str("config.kind"),
1638 Some("sink_defaults")
1639 );
1640 }
1641}