1use std::{
2 cell::RefCell,
3 error::Error as ErrorTrait,
4 fmt, io,
5 num::{ParseFloatError, ParseIntError},
6 result,
7};
8
9use once_cell::sync::Lazy;
10
11use crate::common::arena::SymHashMap;
12
13#[derive(Debug, Clone, Default)]
14pub struct LogState {
15 pub undefined: SymHashMap<usize>,
16 pub missing: SymHashMap<usize>,
17 pub debug: usize,
18 pub info: usize,
19 pub warning: usize,
20 pub error: usize,
21 pub fatal: bool,
22 pub status_code: usize,
23}
24pub enum LogStatus {
25 Debug,
26 Info,
27 Warning,
28 Error,
29 Fatal,
30 Undefined,
31 Missing,
32}
33
34#[thread_local]
35pub static REPORT: Lazy<RefCell<LogState>> = Lazy::new(|| RefCell::new(LogState::default()));
36
37#[thread_local]
41static MACRO_DIAG_DEPTH: std::cell::Cell<u32> = std::cell::Cell::new(0);
42
43pub struct MacroDiagGuard(());
54impl Drop for MacroDiagGuard {
55 fn drop(&mut self) { MACRO_DIAG_DEPTH.set(MACRO_DIAG_DEPTH.get().saturating_sub(1)); }
56}
57#[must_use = "the guard must live across the log emission it marks"]
58pub fn macro_diag_guard() -> MacroDiagGuard {
59 MACRO_DIAG_DEPTH.set(MACRO_DIAG_DEPTH.get() + 1);
60 MacroDiagGuard(())
61}
62
63pub fn note_status_from_logger(status: LogStatus) {
69 if MACRO_DIAG_DEPTH.get() > 0 || REPORT.try_borrow_mut().is_err() {
70 return;
71 }
72 note_status(status, None);
73}
74
75#[thread_local]
81static SUPPRESS_LOG_OUTPUT: std::cell::Cell<bool> = std::cell::Cell::new(false);
82
83pub fn set_suppress_log_output(suppress: bool) -> bool {
85 let prev = SUPPRESS_LOG_OUTPUT.get();
86 SUPPRESS_LOG_OUTPUT.set(suppress);
87 prev
88}
89
90thread_local! {
100 static DEMOTE_FATALS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
101}
102
103pub fn set_demote_fatals(demote: bool) -> bool {
105 DEMOTE_FATALS.with(|c| {
106 let prev = c.get();
107 c.set(demote);
108 prev
109 })
110}
111
112pub fn is_demote_fatals() -> bool { DEMOTE_FATALS.with(|c| c.get()) }
114
115pub fn is_log_output_suppressed() -> bool { SUPPRESS_LOG_OUTPUT.get() }
117
118#[thread_local]
126static LAST_ERROR_KEY: RefCell<Option<String>> = RefCell::new(None);
127#[thread_local]
128static CONSECUTIVE_ERROR_COUNT: std::cell::Cell<usize> = std::cell::Cell::new(0);
129
130pub const MAX_CONSECUTIVE_ERRORS: usize = 500;
140
141pub fn note_consecutive_error(key: &str) -> usize {
145 let mut last = LAST_ERROR_KEY.borrow_mut();
146 if last.as_deref() == Some(key) {
147 let c = CONSECUTIVE_ERROR_COUNT.get() + 1;
148 CONSECUTIVE_ERROR_COUNT.set(c);
149 c
150 } else {
151 *last = Some(key.to_string());
152 CONSECUTIVE_ERROR_COUNT.set(1);
153 1
154 }
155}
156
157pub fn emit_record(status: LogStatus, target: &str, message: &str) {
167 let _diag_guard = macro_diag_guard();
168 let level = match status {
169 LogStatus::Debug => log::Level::Debug,
170 LogStatus::Info => log::Level::Info,
171 LogStatus::Warning => log::Level::Warn,
172 _ => log::Level::Error,
173 };
174 let unconditional = matches!(status, LogStatus::Error | LogStatus::Fatal);
175 note_status(status, None);
176 if unconditional || !is_log_output_suppressed() {
177 log::log!(target: target, level, "{message}");
178 }
179}
180
181pub fn emit_info(category: &str, object: &str, message: &str) {
198 emit_record(LogStatus::Info, &format!("{category}:{object}"), message);
199}
200
201pub fn emit_warn(category: &str, object: &str, message: &str) {
203 emit_record(LogStatus::Warning, &format!("{category}:{object}"), message);
204}
205
206pub fn emit_error(category: &str, object: &str, message: &str) {
213 emit_record(LogStatus::Error, &format!("{category}:{object}"), message);
214 if is_demote_fatals() {
215 return;
216 }
217 let maxerrors = match crate::state::try_lookup_int("MAX_ERRORS") {
218 None => usize::MAX, Some(v) if v > 0 => v as usize,
220 Some(_) => 100,
221 };
222 let consec = note_consecutive_error(&format!("{category}:{object}"));
223 let over_total = get_status(LogStatus::Error) > maxerrors;
224 let over_consec = consec > MAX_CONSECUTIVE_ERRORS;
225 if (over_total && get_status(LogStatus::Error) == maxerrors + 1)
227 || (over_consec && consec == MAX_CONSECUTIVE_ERRORS + 1)
228 {
229 emit_fatal(
230 "TooManyErrors",
231 "MaxLimit",
232 &format!(
233 "Too many errors (> {})!",
234 if over_total {
235 maxerrors
236 } else {
237 MAX_CONSECUTIVE_ERRORS
238 }
239 ),
240 );
241 }
242}
243
244pub fn emit_fatal(category: &str, object: &str, message: &str) {
250 emit_record(
251 LogStatus::Fatal,
252 &format!("Fatal:{category}:{object} "),
253 message,
254 );
255}
256
257fn reset_consecutive_error_tracker() {
259 *LAST_ERROR_KEY.borrow_mut() = None;
260 CONSECUTIVE_ERROR_COUNT.set(0);
261}
262#[macro_export]
263macro_rules! report {
264 () => {
265 (*$crate::common::error::REPORT).borrow()
266 };
267}
268#[macro_export]
269macro_rules! report_mut {
270 () => {
271 (*$crate::common::error::REPORT).borrow_mut()
272 };
273}
274
275pub fn clear_fatal_flag() {
281 let mut report = REPORT.borrow_mut();
282 report.fatal = false;
283}
284
285pub fn note_status(status: LogStatus, what: Option<&str>) {
286 let mut report = REPORT.borrow_mut();
287 use LogStatus::*;
288 match status {
289 Debug => report.debug += 1,
290 Info => report.info += 1,
291 Warning => report.warning += 1,
292 Error => report.error += 1,
293 Fatal => {
294 if !report.fatal && debug_fatal_enabled() {
299 eprintln!("[debug-fatal] LogStatus::Fatal first noted here:");
300 eprintln!("{}", std::backtrace::Backtrace::force_capture());
301 }
302 report.fatal = true;
303 },
304 Undefined => {
305 let key = what.unwrap_or_default().to_string();
310 let entry = report.undefined.entry(&key).or_insert(0);
311 *entry += 1;
312 },
313 Missing => {
314 let key = what.unwrap_or_default().to_string();
315 let entry = report.missing.entry(&key).or_insert(0);
316 *entry += 1;
317 },
318 }
319}
320
321pub fn get_status(status: LogStatus) -> usize {
322 let report = REPORT.borrow();
323 use LogStatus::*;
324 match status {
325 Debug => report.debug,
326 Info => report.info,
327 Warning => report.warning,
328 Error => report.error,
329 Fatal => {
330 if report.fatal {
331 1
332 } else {
333 0
334 }
335 },
336 Undefined => report.undefined.0.values().sum(),
337 Missing => report.missing.0.values().sum(),
338 }
339}
340
341pub fn debug_fatal_enabled() -> bool {
346 use std::sync::OnceLock;
347 static FLAG: OnceLock<bool> = OnceLock::new();
348 *FLAG.get_or_init(|| std::env::var_os("LATEXML_DEBUG_FATAL").is_some())
349}
350
351pub fn initialize_report() {
352 let mut report = REPORT.borrow_mut();
353 *report = LogState::default();
354 reset_consecutive_error_tracker();
355 LAST_RESOURCE_FATAL.with(|c| *c.borrow_mut() = None);
356}
357
358pub fn reset_arena_keyed_reports() {
364 let mut report = REPORT.borrow_mut();
365 report.undefined = Default::default();
366 report.missing = Default::default();
367}
368
369thread_local! {
370 static LAST_RESOURCE_FATAL: RefCell<Option<Error>> = const { RefCell::new(None) };
380}
381
382pub fn record_last_fatal(e: &Error) {
387 use ErrorCategory as C;
388 if !matches!(e.target, ErrorTarget::Timeout) {
389 return;
390 }
391 let category = match &e.category {
392 C::TokenLimit => C::TokenLimit,
393 C::PushbackLimit => C::PushbackLimit,
394 C::Recursion => C::Recursion,
395 C::IfLimit => C::IfLimit,
396 C::MemoryBudget => C::MemoryBudget,
397 C::Convert => C::Convert,
398 _ => return,
399 };
400 LAST_RESOURCE_FATAL.with(|c| {
401 *c.borrow_mut() = Some(Error {
402 target: ErrorTarget::Timeout,
403 category,
404 message: e.message.clone(),
405 });
406 });
407}
408
409pub fn take_last_resource_fatal() -> Option<Error> {
413 LAST_RESOURCE_FATAL.with(|c| c.borrow_mut().take())
414}
415
416pub fn conversion_status_line(code: usize) -> String { format!("Status:conversion:{code}") }
427
428pub fn conversion_verdict(code: usize) -> String {
434 format!(
435 "Conversion {}: {}",
436 if code >= 3 { "failed" } else { "complete" },
437 get_status_message()
438 )
439}
440
441pub fn get_status_message() -> String {
442 let report = REPORT.borrow();
443 let mut parts = Vec::new();
444 if report.warning > 0 {
445 parts.push(format!(
446 "{} warning{}",
447 report.warning,
448 if report.warning > 1 { "s" } else { "" }
449 ));
450 }
451 if report.error > 0 {
452 parts.push(format!(
453 "{} error{}",
454 report.error,
455 if report.error > 1 { "s" } else { "" }
456 ));
457 }
458 if report.fatal {
459 parts.push("1 fatal error".to_string());
460 }
461 let undef_keys: Vec<String> = report
462 .undefined
463 .keys()
464 .map(|k| crate::common::arena::to_string(*k))
465 .collect();
466 if !undef_keys.is_empty() {
467 parts.push(format!(
468 "{} undefined macro{}[{}]",
469 undef_keys.len(),
470 if undef_keys.len() > 1 { "s" } else { "" },
471 undef_keys.join(", ")
472 ));
473 }
474 let miss_keys: Vec<String> = report
475 .missing
476 .keys()
477 .map(|k| crate::common::arena::to_string(*k))
478 .collect();
479 if !miss_keys.is_empty() {
480 parts.push(format!(
481 "{} missing file{}[{}]",
482 miss_keys.len(),
483 if miss_keys.len() > 1 { "s" } else { "" },
484 miss_keys.join(", ")
485 ));
486 }
487 if parts.is_empty() {
488 "No obvious problems".to_string()
489 } else {
490 parts.join("; ")
491 }
492}
493
494pub fn get_status_code() -> usize {
497 let report = REPORT.borrow();
498 if report.fatal {
499 3
500 } else if report.error > 0 {
501 2
502 } else if report.warning > 0 {
503 1
504 } else {
505 0
506 }
507}
508
509#[derive(Default, Clone, Copy)]
518pub struct ReportCounts {
519 pub debug: usize,
520 pub info: usize,
521 pub warning: usize,
522 pub error: usize,
523 pub fatal: bool,
524}
525
526pub fn snapshot_report_counts() -> ReportCounts {
528 let r = REPORT.borrow();
529 ReportCounts {
530 debug: r.debug,
531 info: r.info,
532 warning: r.warning,
533 error: r.error,
534 fatal: r.fatal,
535 }
536}
537
538pub fn restore_report_counts(c: ReportCounts) {
545 let mut r = REPORT.borrow_mut();
546 r.debug = c.debug;
547 r.info = c.info;
548 r.warning = c.warning;
549 r.error = c.error;
550 r.fatal = c.fatal;
551}
552
553pub fn merge_report_counts(c: ReportCounts) {
558 let mut r = REPORT.borrow_mut();
559 r.debug += c.debug;
560 r.info += c.info;
561 r.warning += c.warning;
562 r.error += c.error;
563 r.fatal |= c.fatal;
564}
565
566static KNOWN_DEBUG_FEATURES: Lazy<std::sync::RwLock<std::collections::BTreeSet<String>>> =
575 Lazy::new(|| std::sync::RwLock::new(std::collections::BTreeSet::new()));
576static ENABLED_DEBUG_FEATURES: Lazy<std::sync::RwLock<rustc_hash::FxHashSet<String>>> =
577 Lazy::new(|| std::sync::RwLock::new(rustc_hash::FxHashSet::default()));
578
579pub fn debuggable_feature(name: &str) {
582 if let Ok(mut known) = KNOWN_DEBUG_FEATURES.write() {
583 known.insert(name.to_string());
584 }
585}
586
587pub fn known_debug_features() -> Vec<String> {
589 KNOWN_DEBUG_FEATURES
590 .read()
591 .map(|k| k.iter().cloned().collect())
592 .unwrap_or_default()
593}
594
595pub fn enable_debug_feature(name: &str) {
597 if let Ok(mut enabled) = ENABLED_DEBUG_FEATURES.write() {
598 enabled.insert(name.to_string());
599 }
600}
601
602pub fn debug_enabled(name: &str) -> bool {
604 ENABLED_DEBUG_FEATURES
605 .read()
606 .map(|enabled| enabled.contains(name))
607 .unwrap_or(false)
608}
609
610#[inline]
619pub fn debug_record_enabled() -> bool {
620 log::max_level() >= log::LevelFilter::Debug && !is_log_output_suppressed()
621}
622
623#[macro_export]
631macro_rules! DebugFeature {
632 ($feature:literal, $($arg:tt)*) => {{
633 if $crate::common::error::debug_enabled($feature) {
634 let __diag_guard = $crate::common::error::macro_diag_guard();
635 $crate::common::error::note_status(
636 $crate::common::error::LogStatus::Debug, None);
637 use log::debug;
638 debug!(target: $feature, $($arg)*);
639 }
640 }};
641}
642
643#[macro_export]
650macro_rules! Debug {
651 ($category:expr_2021, $object:expr_2021, $message:expr_2021) => {{
652 if $crate::common::error::debug_record_enabled() {
653 $crate::common::error::emit_record(
654 $crate::common::error::LogStatus::Debug,
655 &format!("{}:{}", $category, $object),
656 &$crate::generate_message!($message))
657 } else {
658 $crate::common::error::note_status(
659 $crate::common::error::LogStatus::Debug, None);
660 }
661 }};
662 ($category:expr_2021, $object:expr_2021, $message:expr_2021, $($details:expr_2021),*) => {{
663 if $crate::common::error::debug_record_enabled() {
664 $crate::common::error::emit_record(
665 $crate::common::error::LogStatus::Debug,
666 &format!("{}:{}", $category, $object),
667 &$crate::generate_message!($message, $($details),*))
668 } else {
669 $crate::common::error::note_status(
670 $crate::common::error::LogStatus::Debug, None);
671 }
672 }};
673 ($($simple:expr_2021),*) => {{
674 $crate::common::error::note_status(
675 $crate::common::error::LogStatus::Debug, None);
676 if $crate::common::error::debug_record_enabled() {
677 let __diag_guard = $crate::common::error::macro_diag_guard();
678 use log::debug;
679 debug!($($simple),*);
680 }
681 }};
682
683}
684
685#[macro_export]
686macro_rules! Info {
687 ($category:expr_2021, $object:expr_2021, $message:expr_2021) => {{
688 $crate::common::error::emit_info(
689 &format!("{}", $category), &format!("{}", $object),
690 &$crate::generate_message!($message))
691 }};
692 ($category:expr_2021, $object:expr_2021, $message:expr_2021, $($details:expr_2021),*) => {{
693 $crate::common::error::emit_info(
694 &format!("{}", $category), &format!("{}", $object),
695 &$crate::generate_message!($message, $($details),*))
696 }};
697 ($($simple:expr_2021),*) => {{
698 let __diag_guard = $crate::common::error::macro_diag_guard();
699 $crate::common::error::note_status(
700 $crate::common::error::LogStatus::Info, None);
701 use log::info;
702 info!($($simple),*);
703 }};
704
705}
706
707#[macro_export]
708macro_rules! Warn {
709 ($category:expr_2021, $object:expr_2021, $message:expr_2021) => {{
710 $crate::common::error::emit_warn(
711 &format!("{}", $category), &format!("{}", $object),
712 &$crate::generate_message!($message))
713 }};
714 ($category:expr_2021, $object:expr_2021, $message:expr_2021, $($details:expr_2021),*) => {{
715 $crate::common::error::emit_warn(
716 &format!("{}", $category), &format!("{}", $object),
717 &$crate::generate_message!($message, $($details),*))
718 }}
719}
720
721#[macro_export]
722macro_rules! Error {
723 ($category:expr_2021, $object:expr_2021, $message:expr_2021) => {{
724 $crate::Error!($category,$object,$message,"")
725 }};
726 ($category:expr_2021, $object:expr_2021, $message:expr_2021, $($details:expr_2021),*) => {{
727 $crate::common::error::emit_record(
728 $crate::common::error::LogStatus::Error,
729 &format!("{}:{}", $category, $object),
730 &$crate::generate_message!($message, $($details),*),
731 );
732 if !$crate::common::error::is_demote_fatals() {
739 let max_from_state = $crate::state::try_lookup_int("MAX_ERRORS");
744 let maxerrors = match max_from_state {
750 None => usize::MAX,
754 Some(v) if v > 0 => v as usize,
755 Some(_) => 100,
756 };
757 if $crate::common::error::get_status($crate::common::error::LogStatus::Error) > maxerrors {
758 Fatal!(TooManyErrors, MaxLimit(maxerrors), format!("Too many errors (> {maxerrors})!"));
759 }
760 let __consec_key = format!("{}:{}", $category, $object);
769 let __consec = $crate::common::error::note_consecutive_error(&__consec_key);
770 if __consec > $crate::common::error::MAX_CONSECUTIVE_ERRORS {
771 Fatal!(
772 TooManyErrors,
773 MaxLimit($crate::common::error::MAX_CONSECUTIVE_ERRORS),
774 format!(
775 "Runaway: same error '{}' fired {} times in a row (cap = {})",
776 __consec_key, __consec, $crate::common::error::MAX_CONSECUTIVE_ERRORS
777 )
778 );
779 }
780 }
781 }}
782}
783
784#[macro_export]
786macro_rules! Fatal {
787 ($target:expr_2021, $category:expr_2021, $message:expr_2021) => {{
788 if $crate::common::error::is_demote_fatals() {
789 $crate::common::error::emit_record(
795 $crate::common::error::LogStatus::Error,
796 "demoted_fatal",
797 &format!("{}", $message),
798 );
799 } else {
800 $crate::common::error::note_status($crate::common::error::LogStatus::Fatal, None);
801 }
802 {
803 use $crate::common::error::{Error as LatexmlError, ErrorCategory::*, ErrorTarget::*};
804 let __fatal_err = LatexmlError {
805 target: $target,
806 category: $category,
807 message: $message.to_string(),
808 };
809 $crate::common::error::record_last_fatal(&__fatal_err);
813 return Err(__fatal_err);
814 }
815 }};
816}
817
818#[macro_export]
819macro_rules! fatal {
820 ($target:expr_2021, $category:expr_2021, $message:expr_2021) => {{
821 use $crate::common::error::{Error as LatexmlError, ErrorCategory::*, ErrorTarget::*};
822 return Err(LatexmlError {
823 target: $target,
824 category: $category,
825 message: $message.to_string(),
826 });
827 }};
828}
829
830#[macro_export]
831macro_rules! generate_message {
832 ($message:expr_2021) => {
833 format!(
834 "{}\n\t{}\n\tIn {}:{}:{}\n",
835 $message,
836 $crate::gullet::get_location(),
837 file!(),
838 line!(),
839 column!()
840 )
841 };
842 ($message:expr_2021, $detail:expr_2021) => {
843 format!(
844 "{}\n\t{}\n\t{}\n\tIn {}:{}:{}\n",
845 $message,
846 $crate::gullet::get_location(),
847 $detail,
848 file!(),
849 line!(),
850 column!()
851 )
852 };
853 ($message:expr_2021, $detail:expr_2021, $detail2:expr_2021) => {
854 format!(
855 "{}\n\t{}\n\t{}\n\t{}\n\tIn {}:{}:{}\n",
856 $message,
857 $crate::gullet::get_location(),
858 $detail,
859 $detail2,
860 file!(),
861 line!(),
862 column!()
863 )
864 };
865 ($message:expr_2021, $detail:expr_2021, $detail2:expr_2021) => {
866 format!(
867 "{}\n\t{}\n\t{}\n\t{}\n\tIn {}:{}:{}\n",
868 $message,
869 $crate::gullet::get_location(),
870 $detail,
871 $detail2,
872 file!(),
873 line!(),
874 column!()
875 )
876 };
877 ($message:expr_2021, $detail:expr_2021, $detail2:expr_2021, $location:expr_2021) => {
878 format!(
879 "{}\n\t{}\n\t{}\n\t{}\n\tIn {}:{}:{}\n",
880 $message,
881 $location,
882 $detail,
883 $detail2,
884 file!(),
885 line!(),
886 column!()
887 )
888 };
889}
890
891#[macro_export]
898macro_rules! Note {
899 ($input:expr_2021) => {{
900 let msg = $input;
901 $crate::util::logger::note_to_log(&msg.to_string());
902 if !$crate::common::error::is_log_output_suppressed()
903 && $crate::util::logger::stderr_shows_info()
904 {
905 $crate::println_stderr!("{msg}");
906 $crate::util::logger::mark_stderr_at_line_start();
907 }
908 }};
909}
910
911#[macro_export]
914macro_rules! NoteLog {
915 ($input:expr_2021) => {
916 $crate::util::logger::note_to_log(&($input).to_string());
917 };
918}
919
920#[macro_export]
925macro_rules! NoteSTDERR {
926 ($input:expr_2021) => {
927 if !$crate::common::error::is_log_output_suppressed()
928 && $crate::util::logger::stderr_shows_info()
929 {
930 let msg = $input;
931 $crate::println_stderr!("{msg}");
932 $crate::util::logger::mark_stderr_at_line_start();
933 }
934 };
935}
936
937pub type Result<T> = result::Result<T, Error>;
938
939#[derive(Debug)]
940pub struct Error {
941 pub target: ErrorTarget,
942 pub category: ErrorCategory,
943 pub message: String,
944}
945impl ErrorTrait for Error {}
946unsafe impl Send for Error {}
952unsafe impl Sync for Error {}
953
954#[derive(Debug)]
955pub enum ErrorCategory {
956 Init,
957 Io(io::Error),
958 NotFound,
959 Unexpected,
960 Expected,
961 Misdefined,
962 Unknown,
963 MissingFile,
964 Malformed,
965 Libxml,
966 Convert,
967 Recursion,
968 EoF,
969 Endgroup,
970 FailedParse,
971 MaxLimit(usize),
972 Generic(Box<dyn ErrorTrait>),
973 Filename(String),
974 ToDo,
975 TokenLimit,
976 PushbackLimit,
977 IfLimit,
978 MemoryBudget,
979}
980
981#[derive(Debug)]
982pub enum ErrorTarget {
983 Package,
984 Parameter,
985 ParamSpec,
986 Prototype,
987 Converter,
988 Mouth,
989 Core,
990 State,
991 Stomach,
992 Codegen,
993 Macro,
994 XMath,
995 MathParser,
996 Document,
997 Definition,
998 TexPool,
999 Internal,
1000 TargetUnexpected,
1001 TooManyErrors,
1002 Timeout,
1003}
1004
1005impl fmt::Display for ErrorCategory {
1006 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1007 use ErrorCategory::*;
1008 match self {
1009 Init => write!(f, "Init"),
1010 Io(err) => err.fmt(f),
1011 NotFound => write!(f, "No matching cities with a population were found."),
1012 MissingFile => write!(f, "missing file"),
1013 Misdefined => write!(f, "misdefined"),
1014 Unknown => write!(f, "unknown"),
1015 Malformed => write!(f, "malformed"),
1016 Expected => write!(f, "expected"),
1017 Unexpected => write!(f, "unexpected"),
1018 Libxml => write!(f, "libxml error"),
1019 Recursion => write!(f, "<recursion>"),
1020 EoF => write!(f, "<EOF>"),
1021 ToDo => write!(f, "TODO"),
1022 Convert => write!(f, "conversion"),
1023 Endgroup => write!(f, "<endgroup>"),
1024 FailedParse => write!(f, "failed to parse"),
1025 MaxLimit(num) => write!(f, "{}", num),
1026 Generic(err) => err.fmt(f),
1027 Filename(name) => write!(f, "file:{name}"),
1028 TokenLimit => write!(f, "token_limit"),
1029 PushbackLimit => write!(f, "pushback_limit"),
1030 IfLimit => write!(f, "if_limit"),
1031 MemoryBudget => write!(f, "memory_budget"),
1032 }
1033 }
1034}
1035impl fmt::Display for Error {
1036 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1037 write!(
1038 f,
1039 "Error:{}:{:?} {}",
1040 self.category, self.target, self.message
1041 )
1042 }
1043}
1044
1045impl Error {
1046 pub fn log_fatal(&self) {
1047 emit_record(
1053 LogStatus::Fatal,
1054 &s!("Fatal:{:?}:{:?} ", self.target, self.category),
1055 &self.message,
1056 );
1057 }
1058 pub fn todo() -> Self {
1059 Error {
1060 target: ErrorTarget::Internal,
1061 category: ErrorCategory::ToDo,
1062 message: String::from(
1063 "This section of the code is not yet implemented / ported over from Perl.",
1064 ),
1065 }
1066 }
1067}
1068
1069#[macro_export]
1070macro_rules! unported {
1071 () => {{ ::latexml_core::common::error::Error::todo() }};
1072}
1073
1074impl From<io::Error> for Error {
1075 fn from(err: io::Error) -> Error {
1076 Error {
1077 target: ErrorTarget::Mouth,
1078 category: ErrorCategory::Io(err),
1079 message: s!("IO error"),
1080 }
1081 }
1082}
1083
1084impl From<Box<dyn ErrorTrait>> for Error {
1085 fn from(err: Box<dyn ErrorTrait>) -> Error {
1086 Error {
1087 target: ErrorTarget::Document,
1088 message: err.to_string(),
1089 category: ErrorCategory::Generic(err),
1090 }
1091 }
1092}
1093impl From<Box<dyn ErrorTrait + Send + Sync>> for Error {
1094 fn from(err: Box<dyn ErrorTrait + Send + Sync>) -> Error {
1095 Error {
1096 target: ErrorTarget::Document,
1097 message: err.to_string(),
1098 category: ErrorCategory::Generic(err),
1099 }
1100 }
1101}
1102
1103impl From<String> for Error {
1104 fn from(err: String) -> Error {
1105 Error {
1106 target: ErrorTarget::Document,
1107 category: ErrorCategory::Generic(From::from(err.clone())),
1108 message: err,
1109 }
1110 }
1111}
1112
1113impl<'a> From<&'a str> for Error {
1114 fn from(err: &'a str) -> Error {
1115 Error {
1116 target: ErrorTarget::Document,
1117 category: ErrorCategory::Generic(From::from(err.to_owned())),
1118 message: err.to_owned(),
1119 }
1120 }
1121}
1122
1123impl From<()> for Error {
1124 fn from(_e: ()) -> Error {
1125 Error {
1126 target: ErrorTarget::Document,
1127 category: ErrorCategory::Libxml,
1128 message: s!("LibXML error"),
1129 }
1130 }
1131}
1132
1133impl From<ParseIntError> for Error {
1134 fn from(err: ParseIntError) -> Error {
1135 Error {
1136 target: ErrorTarget::Document,
1137 message: err.to_string(),
1138 category: ErrorCategory::Generic(Box::new(err)),
1139 }
1140 }
1141}
1142
1143impl From<ParseFloatError> for Error {
1144 fn from(err: ParseFloatError) -> Error {
1145 Error {
1146 target: ErrorTarget::Document,
1147 message: err.to_string(),
1148 category: ErrorCategory::Generic(Box::new(err)),
1149 }
1150 }
1151}
1152
1153impl From<marpa::error::Error> for Error {
1154 fn from(err: marpa::error::Error) -> Error {
1155 Error {
1156 target: ErrorTarget::MathParser,
1157 category: ErrorCategory::FailedParse,
1158 message: err.to_string(),
1159 }
1160 }
1161}
1162
1163pub fn progress_step(_note: &str) {
1176 }
1179
1180pub fn note_progress(stuff: &str) {
1181 use log::info;
1182 info!(target: "note", "{}", stuff);
1183}
1184
1185pub fn note_progress_detailed(stuff: &str) {
1187 use log::debug;
1188 debug!(target: "note", "{}", stuff);
1189}
1190
1191pub fn note_begin(stage: &str) {
1200 use log::info;
1202 info!(target: "note", "\n({}...", stage);
1203}
1204
1205pub fn note_end(_stage: &str) {
1211 use log::info;
1217 info!(target: "note", " )");
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 #[test]
1231 fn initialize_report_clears_state() {
1232 note_status(LogStatus::Warning, None);
1233 initialize_report();
1234 assert_eq!(get_status(LogStatus::Warning), 0);
1235 }
1236
1237 #[test]
1238 fn note_status_increments_counters() {
1239 initialize_report();
1240 note_status(LogStatus::Warning, None);
1241 note_status(LogStatus::Warning, None);
1242 note_status(LogStatus::Error, None);
1243 assert_eq!(get_status(LogStatus::Warning), 2);
1244 assert_eq!(get_status(LogStatus::Error), 1);
1245 assert_eq!(get_status(LogStatus::Fatal), 0);
1246 }
1247
1248 #[test]
1249 fn fatal_macro_latches_resource_fatals() {
1250 initialize_report();
1254 fn raise() -> Result<()> {
1255 Fatal!(
1256 Timeout,
1257 TokenLimit,
1258 "Token limit of 5 exceeded, infinite loop?"
1259 );
1260 }
1261 let err = raise().unwrap_err();
1262 assert!(matches!(err.target, ErrorTarget::Timeout));
1263 let latched = take_last_resource_fatal().expect("latch must hold the fatal");
1264 assert!(matches!(latched.target, ErrorTarget::Timeout));
1265 assert!(matches!(latched.category, ErrorCategory::TokenLimit));
1266 assert_eq!(latched.message, "Token limit of 5 exceeded, infinite loop?");
1267 assert!(take_last_resource_fatal().is_none());
1269 fn raise_other() -> Result<()> {
1271 Fatal!(Internal, EoF, "fell off the end");
1272 }
1273 let _ = raise_other().unwrap_err();
1274 assert!(take_last_resource_fatal().is_none());
1275 let _ = raise().unwrap_err();
1277 initialize_report();
1278 assert!(take_last_resource_fatal().is_none());
1279 }
1280
1281 #[test]
1282 fn fatal_status_is_sticky_and_returns_1() {
1283 initialize_report();
1284 note_status(LogStatus::Fatal, None);
1285 note_status(LogStatus::Fatal, None);
1286 assert_eq!(get_status(LogStatus::Fatal), 1);
1288 }
1289
1290 #[test]
1291 fn get_status_code_priority_order() {
1292 initialize_report();
1293 assert_eq!(get_status_code(), 0, "clean state → 0");
1294 note_status(LogStatus::Warning, None);
1295 assert_eq!(get_status_code(), 1, "warning → 1");
1296 note_status(LogStatus::Error, None);
1297 assert_eq!(get_status_code(), 2, "error wins over warning → 2");
1298 note_status(LogStatus::Fatal, None);
1299 assert_eq!(get_status_code(), 3, "fatal wins over error → 3");
1300 }
1301
1302 #[test]
1303 fn status_message_clean_is_no_obvious_problems() {
1304 initialize_report();
1305 assert_eq!(get_status_message(), "No obvious problems");
1306 }
1307
1308 #[test]
1309 fn status_message_plural_warnings() {
1310 initialize_report();
1311 note_status(LogStatus::Warning, None);
1312 let m = get_status_message();
1313 assert_eq!(m, "1 warning", "singular form");
1314
1315 note_status(LogStatus::Warning, None);
1316 let m = get_status_message();
1317 assert_eq!(m, "2 warnings", "plural form");
1318 }
1319
1320 #[test]
1321 fn status_message_multiple_categories_joined() {
1322 initialize_report();
1323 note_status(LogStatus::Warning, None);
1324 note_status(LogStatus::Warning, None);
1325 note_status(LogStatus::Error, None);
1326 let m = get_status_message();
1327 assert!(
1328 m.contains("2 warnings") && m.contains("1 error") && m.contains("; "),
1329 "got {m:?}"
1330 );
1331 }
1332
1333 #[test]
1334 fn suppress_log_output_returns_prior_value() {
1335 let prior = set_suppress_log_output(true);
1336 assert!(is_log_output_suppressed());
1337 let prior2 = set_suppress_log_output(false);
1338 assert!(prior2, "round-trip prior value");
1339 assert!(!is_log_output_suppressed());
1340 set_suppress_log_output(prior);
1342 }
1343}