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