1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", "README.md"))]
2
3use once_cell::sync::Lazy;
4use std::collections::HashSet;
5use std::env;
6use std::fs::{self, OpenOptions};
7use std::io::Write;
8use std::path::PathBuf;
9
10pub static DEBUG_DIR: Lazy<PathBuf> = Lazy::new(|| {
11 let debug_dir = determine_debug_dir();
12 fs::create_dir_all(&debug_dir).unwrap_or_else(|e| {
13 eprintln!("Failed to create debug directory: {}", e);
14 });
15 debug_dir
16});
17
18fn determine_debug_dir() -> PathBuf {
20 #[cfg(feature = "output_to_target")]
21 {
22 find_target_dir()
23 .map(|dir| dir.join("odebug"))
24 .unwrap_or_else(|| {
25 eprintln!(
26 "Warning: Could not find target directory, falling back to default location"
27 );
28 default_debug_dir()
29 })
30 }
31
32 #[cfg(not(feature = "output_to_target"))]
33 {
34 default_debug_dir()
35 }
36}
37
38fn default_debug_dir() -> PathBuf {
40 #[cfg(feature = "use_workspace")]
41 {
42 find_workspace_root()
43 .map(|root| root.join(".debug"))
44 .unwrap_or_else(|| {
45 eprintln!(
46 "Warning: Could not find workspace root, falling back to current directory"
47 );
48 std::env::current_dir().unwrap_or_default().join(".debug")
49 })
50 }
51
52 #[cfg(not(feature = "use_workspace"))]
53 {
54 std::env::current_dir().unwrap_or_default().join(".debug")
55 }
56}
57
58#[cfg(feature = "output_to_target")]
59fn find_target_dir() -> Option<PathBuf> {
60 if let Ok(dir) = std::env::var("CARGO_TARGET_DIR") {
61 return Some(PathBuf::from(dir));
62 }
63
64 #[cfg(feature = "use_workspace")]
65 {
66 if let Some(ws_root) = find_workspace_root() {
67 return Some(ws_root.join("target"));
68 }
69 }
70
71 let current = std::env::current_dir().ok()?;
72 Some(current.join("target"))
73}
74
75#[cfg(feature = "use_workspace")]
76fn find_workspace_root() -> Option<PathBuf> {
77 let mut current_dir = env::current_dir().ok()?;
78 loop {
79 let cargo_toml_path = current_dir.join("Cargo.toml");
80 if cargo_toml_path.exists() {
81 if let Ok(content) = fs::read_to_string(&cargo_toml_path) {
82 if content.contains("[workspace]") {
83 return Some(current_dir);
84 }
85 }
86 }
87 if !current_dir.pop() {
88 break;
89 }
90 }
91 None
92}
93
94#[doc(hidden)]
95const SEPARATOR_LINE: &str = "-----------------------------------------------------------";
96
97#[doc(hidden)]
98static INITIALIZED_FILES: Lazy<std::sync::Mutex<HashSet<String>>> =
99 Lazy::new(|| std::sync::Mutex::new(HashSet::new()));
100
101pub fn write_to_debug_file(
126 filename: &str,
127 content: &str,
128 header: Option<&str>,
129 context: Option<&str>,
130) -> std::io::Result<()> {
131 let _ = fs::create_dir_all(&*DEBUG_DIR);
132
133 let path = DEBUG_DIR.join(filename);
134
135 let should_clear = {
136 let mut initialized = INITIALIZED_FILES.lock().unwrap();
137 if !initialized.contains(filename) {
138 initialized.insert(filename.to_string());
139 true
140 } else {
141 false
142 }
143 };
144
145 if should_clear {
146 let _ = fs::remove_file(&path);
147 }
148
149 let file = OpenOptions::new().create(true).append(true).open(&path)?;
151 let mut writer = std::io::BufWriter::new(file);
152
153 match (header, context) {
154 (Some(header), Some(context)) => {
155 writeln!(writer, "\n{0}", SEPARATOR_LINE)?;
156 writeln!(writer, "> {0} ({1})", header, context)?;
157 writeln!(writer, "{0}", SEPARATOR_LINE)?;
158 writeln!(writer, "{0}", content)?;
159 },
160 (Some(header), None) => {
161 writeln!(writer, "\n{0}", SEPARATOR_LINE)?;
162 writeln!(writer, "> {0}", header)?;
163 writeln!(writer, "{0}", SEPARATOR_LINE)?;
164 writeln!(writer, "{0}", content)?;
165 },
166 (None, Some(context)) => {
167 writeln!(writer, "\n{0}", SEPARATOR_LINE)?;
168 writeln!(writer, "> [at {0}]", context)?;
169 writeln!(writer, "{0}", SEPARATOR_LINE)?;
170 writeln!(writer, "{0}", content)?;
171 },
172 (None, None) => {
173 writeln!(writer, "\n{0}", content)?;
174 },
175 }
176
177 writer.flush()?;
178
179 Ok(())
180}
181
182#[macro_export]
183macro_rules! odebug {
219 ($($args:tt)*) => {
220 #[cfg(any(debug_assertions, feature = "always_log"))]
221 {
222 $crate::__internal_debug_macro!($($args)*)
223 }
224 };
225}
226
227#[doc(hidden)]
228#[macro_export]
229macro_rules! __internal_debug_macro {
230 ($file:ident::$header:ident($content:expr)) => {{
232 let context = format!("{}:{}", file!(), line!());
233 $crate::write_to_debug_file(
234 &format!("{}.log", stringify!($file)),
235 &$content.to_string(),
236 Some(stringify!($header)),
237 Some(&context)
238 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
239 }};
240
241 ($file:ident::$header:ident($fmt:expr, $($arg:tt)+)) => {{
243 let context = format!("{}:{}", file!(), line!());
244 let content = format!($fmt, $($arg)+);
245 $crate::write_to_debug_file(
246 &format!("{}.log", stringify!($file)),
247 &content,
248 Some(stringify!($header)),
249 Some(&context)
250 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
251 }};
252
253 ($file:ident::($content:expr)) => {{
255 let context = format!("{}:{}", file!(), line!());
256 $crate::write_to_debug_file(
257 &format!("{}.log", stringify!($file)),
258 &$content.to_string(),
259 None,
260 Some(&context)
261 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
262 }};
263
264 ($file:ident::($fmt:expr, $($arg:tt)+)) => {{
266 let context = format!("{}:{}", file!(), line!());
267 let content = format!($fmt, $($arg)+);
268 $crate::write_to_debug_file(
269 &format!("{}.log", stringify!($file)),
270 &content,
271 None,
272 Some(&context)
273 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
274 }};
275
276 (::$header:ident($content:expr)) => {{
278 let context = format!("{}:{}", file!(), line!());
279 $crate::write_to_debug_file(
280 "debug.log",
281 &$content.to_string(),
282 Some(stringify!($header)),
283 Some(&context)
284 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
285 }};
286
287 (::$header:ident($fmt:expr, $($arg:tt)+)) => {{
289 let context = format!("{}:{}", file!(), line!());
290 let content = format!($fmt, $($arg)+);
291 $crate::write_to_debug_file(
292 "debug.log",
293 &content,
294 Some(stringify!($header)),
295 Some(&context)
296 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
297 }};
298
299 ($file:expr => $content:expr) => {{
301 let context = format!("{}:{}", file!(), line!());
302 $crate::write_to_debug_file(
303 $file,
304 &$content.to_string(),
305 None,
306 Some(&context)
307 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
308 }};
309
310 ($file:expr => $fmt:expr, $($arg:tt)+) => {{
312 let context = format!("{}:{}", file!(), line!());
313 let content = format!($fmt, $($arg)*);
314 $crate::write_to_debug_file(
315 $file,
316 &content,
317 None,
318 Some(&context)
319 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
320 }};
321
322 ($content:literal.to_file($file:expr)) => {{
324 let context = format!("{}:{}", file!(), line!());
325 $crate::write_to_debug_file(
326 $file,
327 &$content.to_string(),
328 None,
329 Some(&context)
330 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
331 }};
332
333 ($content:literal.with_header($header:expr)) => {{
334 let context = format!("{}:{}", file!(), line!());
335 $crate::write_to_debug_file(
336 "debug.log",
337 &$content.to_string(),
338 Some(&$header.to_string()),
339 Some(&context)
340 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
341 }};
342
343 ($content:literal.to_file($file:expr).with_header($header:expr)) => {{
345 let context = format!("{}:{}", file!(), line!());
346 $crate::write_to_debug_file(
347 $file,
348 &$content.to_string(),
349 Some(&$header.to_string()),
350 Some(&context)
351 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
352 }};
353
354 ($content:ident.to_file($file:expr)) => {{
356 let context = format!("{}:{}", file!(), line!());
357 $crate::write_to_debug_file(
358 $file,
359 &$content.to_string(),
360 None,
361 Some(&context)
362 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
363 }};
364
365 ($content:ident.with_header($header:expr)) => {{
366 let context = format!("{}:{}", file!(), line!());
367 $crate::write_to_debug_file(
368 "debug.log",
369 &$content.to_string(),
370 Some(&$header.to_string()),
371 Some(&context)
372 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
373 }};
374
375 ($content:ident.to_file($file:expr).with_header($header:expr)) => {{
376 let context = format!("{}:{}", file!(), line!());
377 $crate::write_to_debug_file(
378 $file,
379 &$content.to_string(),
380 Some(&$header.to_string()),
381 Some(&context)
382 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
383 }};
384
385 ($content:expr) => {{
387 let context = format!("{}:{}", file!(), line!());
388 $crate::write_to_debug_file(
389 "debug.log",
390 &$content.to_string(),
391 None,
392 Some(&context)
393 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
394 }};
395
396 ($fmt:expr, $($arg:tt)+) => {{
398 let context = format!("{}:{}", file!(), line!());
399 let content = format!($fmt, $($arg)+);
400 $crate::write_to_debug_file(
401 "debug.log",
402 &content,
403 None,
404 Some(&context)
405 ).unwrap_or_else(|e| eprintln!("Failed to write debug log: {}", e))
406 }};
407}
408
409#[cfg(test)]
410mod tests {
411 use once_cell::sync::Lazy;
412 use std::fs;
413 use std::path::Path;
414 use std::sync::Mutex;
415
416 static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
417
418 fn cleanup_test_logs() {
419 let debug_dir = crate::DEBUG_DIR.as_path();
420 let files = ["debug.log", "custom.log", "test.log"];
421 for file in files {
422 let _ = fs::remove_file(debug_dir.join(file));
423 }
424 }
425
426 #[test]
427 fn test_default_variants() {
428 let _guard = TEST_MUTEX.lock().unwrap();
429 cleanup_test_logs();
430
431 odebug!("Test value: {}", 42);
433
434 odebug!("Plain message");
436
437 odebug!(::TestHeader("Test content"));
439
440 let path = crate::DEBUG_DIR.join("debug.log");
442 assert!(Path::new(&path).exists(), "debug.log should exist");
443
444 let content = fs::read_to_string(path).unwrap();
446 let expected_values = ["Test value: 42", "Plain message", "TestHeader", "Test content"];
447
448 for expected in expected_values {
449 assert!(
450 content.contains(expected),
451 "Log should contain: '{}'",
452 expected
453 );
454 }
455 }
456
457 #[test]
458 fn test_custom_filename_variants() {
459 let _guard = TEST_MUTEX.lock().unwrap();
460 cleanup_test_logs();
461
462 odebug!(custom::("Test value: {}", 42));
464 odebug!(custom::("Plain message"));
465 odebug!(custom::TestHeader("Test content"));
466 odebug!("custom.log" => "Alternative content");
467
468 let path = crate::DEBUG_DIR.join("custom.log");
470 assert!(Path::new(&path).exists(), "custom.log should exist");
471
472 let content = fs::read_to_string(path).unwrap();
474 let expected_values = [
475 "Test value: 42",
476 "Plain message",
477 "TestHeader",
478 "Test content",
479 "Alternative content",
480 ];
481
482 for expected in expected_values {
483 assert!(
484 content.contains(expected),
485 "Log should contain: '{}'",
486 expected
487 );
488 }
489 }
490
491 #[test]
492 fn test_string_literal_filename_variants() {
493 let _guard = TEST_MUTEX.lock().unwrap();
494 cleanup_test_logs();
495
496 odebug!("test.log" => "Test value: {}", 42);
498 odebug!("test.log" => "Plain message");
499 odebug!("test.log" => "Test content");
500
501 let path = crate::DEBUG_DIR.join("test.log");
503 assert!(Path::new(&path).exists(), "test.log should exist");
504
505 let content = fs::read_to_string(path).unwrap();
507 let expected_values = ["Test value: 42", "Plain message", "Test content"];
508
509 for expected in expected_values {
510 assert!(
511 content.contains(expected),
512 "Log should contain: '{}'",
513 expected
514 );
515 }
516 }
517
518 #[test]
519 fn test_literal_method_chaining() {
520 let _guard = TEST_MUTEX.lock().unwrap();
521 cleanup_test_logs();
522
523 odebug!("Message".to_file("chain.log"));
525 odebug!("Message".with_header("Test Header"));
526 odebug!("Message".to_file("chain.log").with_header("Combined"));
527
528 let debug_path = crate::DEBUG_DIR.join("debug.log");
530 let chain_path = crate::DEBUG_DIR.join("chain.log");
531
532 assert!(Path::new(&debug_path).exists(), "debug.log should exist");
533 assert!(Path::new(&chain_path).exists(), "chain.log should exist");
534
535 let debug_content = fs::read_to_string(debug_path).unwrap();
537 let chain_content = fs::read_to_string(chain_path).unwrap();
538
539 assert!(
540 debug_content.contains("Test Header"),
541 "debug.log should contain the header"
542 );
543 assert!(
544 chain_content.contains("Message"),
545 "chain.log should contain the message"
546 );
547 assert!(
548 chain_content.contains("Combined"),
549 "chain.log should contain the combined header"
550 );
551 }
552
553 #[test]
554 fn test_identifier_method_chaining() {
555 let _guard = TEST_MUTEX.lock().unwrap();
556 cleanup_test_logs();
557
558 let message = "Variable message".to_string();
560 let header = "Variable header".to_string();
561
562 odebug!(message.to_file("var.log"));
564 odebug!(message.with_header(header));
565 odebug!(message.to_file("var.log").with_header("Combined"));
566
567 let debug_path = crate::DEBUG_DIR.join("debug.log");
569 let var_path = crate::DEBUG_DIR.join("var.log");
570
571 assert!(Path::new(&debug_path).exists(), "debug.log should exist");
572 assert!(Path::new(&var_path).exists(), "var.log should exist");
573
574 let debug_content = fs::read_to_string(debug_path).unwrap();
576 let var_content = fs::read_to_string(var_path).unwrap();
577
578 assert!(
579 debug_content.contains("Variable header"),
580 "debug.log should contain the variable header"
581 );
582 assert!(
583 var_content.contains("Variable message"),
584 "var.log should contain the variable message"
585 );
586 assert!(
587 var_content.contains("Combined"),
588 "var.log should contain the combined header"
589 );
590 }
591}
592
593#[cfg(test)]
594mod feature_tests {
595 use super::*;
596 use std::sync::Mutex;
597
598 static ENV_TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
599
600 fn get_debug_dir_path() -> PathBuf {
601 determine_debug_dir()
602 }
603
604 #[test]
605 fn test_debug_dir_location() {
606 let _guard = ENV_TEST_MUTEX.lock().unwrap();
607 let dir = get_debug_dir_path();
608 println!("Debug directory would be: {}", dir.display());
609
610 #[cfg(feature = "output_to_target")]
611 {
612 assert!(
614 dir.to_string_lossy().contains("target/odebug")
615 || dir.to_string_lossy().contains("target\\odebug"),
616 "Default path should contain 'target/odebug'"
617 );
618
619 assert!(
620 dir.file_name().map_or(false, |name| name == "odebug"),
621 "Path should end with 'odebug' directory"
622 );
623 }
624
625 #[cfg(all(not(feature = "output_to_target"), feature = "use_workspace"))]
627 {
628 assert!(
629 dir.ends_with(".debug"),
630 "With use_workspace enabled, path should end with '.debug'"
631 );
632 }
633
634 #[cfg(all(not(feature = "output_to_target"), not(feature = "use_workspace")))]
635 {
636 let expected = std::env::current_dir().unwrap_or_default().join(".debug");
637 assert_eq!(dir, expected, "Default path should be current_dir/.debug");
638 }
639 }
640
641 #[test]
643 #[cfg(feature = "output_to_target")]
644 fn test_target_dir_env_var() {
645 let _guard = ENV_TEST_MUTEX.lock().unwrap();
646 let test_dir = "/tmp/test_target_dir";
647 std::env::set_var("CARGO_TARGET_DIR", test_dir);
648
649 let dir = get_debug_dir_path();
650
651 assert!(
652 dir.starts_with(test_dir),
653 "Should use CARGO_TARGET_DIR when set"
654 );
655
656 std::env::remove_var("CARGO_TARGET_DIR");
657 }
658}