1use crate::types::Index;
16use std::cell::RefCell;
17use std::fs::{File, OpenOptions};
18use std::io::{self, Write};
19use std::sync::Arc;
20use std::sync::Mutex;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24#[repr(i32)]
25#[allow(non_camel_case_types)]
26pub enum JournalLevel {
27 J_INSUPPRESSIBLE = -1,
28 J_NONE = 0,
29 J_ERROR = 1,
30 J_STRONGWARNING = 2,
31 J_SUMMARY = 3,
32 J_WARNING = 4,
33 J_ITERSUMMARY = 5,
34 J_DETAILED = 6,
35 J_MOREDETAILED = 7,
36 J_VECTOR = 8,
37 J_MOREVECTOR = 9,
38 J_MATRIX = 10,
39 J_MOREMATRIX = 11,
40 J_ALL = 12,
41}
42
43impl JournalLevel {
44 pub const J_LAST_LEVEL: i32 = 13;
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[repr(usize)]
50#[allow(non_camel_case_types)]
51pub enum JournalCategory {
52 J_DBG = 0,
53 J_STATISTICS = 1,
54 J_MAIN = 2,
55 J_INITIALIZATION = 3,
56 J_BARRIER_UPDATE = 4,
57 J_SOLVE_PD_SYSTEM = 5,
58 J_FRAC_TO_BOUND = 6,
59 J_LINEAR_ALGEBRA = 7,
60 J_LINE_SEARCH = 8,
61 J_HESSIAN_APPROXIMATION = 9,
62 J_SOLUTION = 10,
63 J_DOCUMENTATION = 11,
64 J_NLP = 12,
65 J_TIMING_STATISTICS = 13,
66 J_USER_APPLICATION = 14,
67 J_USER1 = 15,
68 J_USER2 = 16,
69 J_USER3 = 17,
70 J_USER4 = 18,
71 J_USER5 = 19,
72 J_USER6 = 20,
73 J_USER7 = 21,
74 J_USER8 = 22,
75 J_USER9 = 23,
76 J_USER10 = 24,
77 J_USER11 = 25,
78 J_USER12 = 26,
79 J_USER13 = 27,
80 J_USER14 = 28,
81 J_USER15 = 29,
82 J_USER16 = 30,
83 J_USER17 = 31,
84}
85
86impl JournalCategory {
87 pub const J_LAST_CATEGORY: usize = 32;
88}
89
90pub trait Journal: Send + Sync {
93 fn name(&self) -> &str;
94
95 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool;
98
99 fn set_print_level(&self, category: JournalCategory, level: JournalLevel);
100
101 fn set_all_print_levels(&self, level: JournalLevel);
102
103 fn print(&self, category: JournalCategory, level: JournalLevel, s: &str);
105
106 fn flush(&self);
107}
108
109struct LevelTable {
111 levels: [i32; JournalCategory::J_LAST_CATEGORY],
112}
113
114impl LevelTable {
115 fn new(default_level: JournalLevel) -> Self {
116 Self {
117 levels: [default_level as i32; JournalCategory::J_LAST_CATEGORY],
118 }
119 }
120
121 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
122 if (level as i32) == JournalLevel::J_INSUPPRESSIBLE as i32 {
124 return true;
125 }
126 (level as i32) <= self.levels[category as usize]
127 }
128
129 fn set_level(&mut self, category: JournalCategory, level: JournalLevel) {
130 self.levels[category as usize] = level as i32;
131 }
132
133 fn set_all(&mut self, level: JournalLevel) {
134 for v in &mut self.levels {
135 *v = level as i32;
136 }
137 }
138}
139
140enum FileSink {
141 Stdout,
142 Stderr,
143 File(File),
144}
145
146impl FileSink {
147 fn write(&mut self, s: &str) -> io::Result<()> {
148 match self {
149 FileSink::Stdout => io::stdout().write_all(s.as_bytes()),
150 FileSink::Stderr => io::stderr().write_all(s.as_bytes()),
151 FileSink::File(f) => f.write_all(s.as_bytes()),
152 }
153 }
154 fn flush(&mut self) -> io::Result<()> {
155 match self {
156 FileSink::Stdout => io::stdout().flush(),
157 FileSink::Stderr => io::stderr().flush(),
158 FileSink::File(f) => f.flush(),
159 }
160 }
161}
162
163pub struct FileJournal {
165 name: String,
166 levels: Mutex<LevelTable>,
167 sink: Mutex<FileSink>,
168}
169
170impl FileJournal {
171 pub fn new(name: impl Into<String>, default_level: JournalLevel) -> Self {
172 Self {
173 name: name.into(),
174 levels: Mutex::new(LevelTable::new(default_level)),
175 sink: Mutex::new(FileSink::Stdout),
176 }
177 }
178
179 pub fn open(&self, fname: &str, append: bool) -> bool {
183 let new_sink = match fname {
184 "stdout" => FileSink::Stdout,
185 "stderr" => FileSink::Stderr,
186 other => {
187 let mut opts = OpenOptions::new();
188 opts.write(true).create(true);
189 if append {
190 opts.append(true);
191 } else {
192 opts.truncate(true);
193 }
194 match opts.open(other) {
195 Ok(f) => FileSink::File(f),
196 Err(_) => return false,
197 }
198 }
199 };
200 match self.sink.lock() {
201 Ok(mut s) => {
202 *s = new_sink;
203 true
204 }
205 _ => false,
206 }
207 }
208}
209
210impl Journal for FileJournal {
211 fn name(&self) -> &str {
212 &self.name
213 }
214
215 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
216 self.levels
217 .lock()
218 .map(|t| t.is_accepted(category, level))
219 .unwrap_or(false)
220 }
221
222 fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
223 if let Ok(mut t) = self.levels.lock() {
224 t.set_level(category, level);
225 }
226 }
227
228 fn set_all_print_levels(&self, level: JournalLevel) {
229 if let Ok(mut t) = self.levels.lock() {
230 t.set_all(level);
231 }
232 }
233
234 fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
235 if !self.is_accepted(category, level) {
236 return;
237 }
238 if let Ok(mut sink) = self.sink.lock() {
239 let _ = sink.write(s);
240 }
241 }
242
243 fn flush(&self) {
244 if let Ok(mut sink) = self.sink.lock() {
245 let _ = sink.flush();
246 }
247 }
248}
249
250pub struct StringJournal {
252 name: String,
253 levels: Mutex<LevelTable>,
254 buffer: Mutex<String>,
255}
256
257impl StringJournal {
258 pub fn new(name: impl Into<String>, default_level: JournalLevel) -> Self {
259 Self {
260 name: name.into(),
261 levels: Mutex::new(LevelTable::new(default_level)),
262 buffer: Mutex::new(String::new()),
263 }
264 }
265
266 pub fn contents(&self) -> String {
267 self.buffer.lock().map(|b| b.clone()).unwrap_or_default()
268 }
269
270 pub fn take(&self) -> String {
271 self.buffer
272 .lock()
273 .map(|mut b| std::mem::take(&mut *b))
274 .unwrap_or_default()
275 }
276}
277
278impl Journal for StringJournal {
279 fn name(&self) -> &str {
280 &self.name
281 }
282
283 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
284 self.levels
285 .lock()
286 .map(|t| t.is_accepted(category, level))
287 .unwrap_or(false)
288 }
289
290 fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
291 if let Ok(mut t) = self.levels.lock() {
292 t.set_level(category, level);
293 }
294 }
295
296 fn set_all_print_levels(&self, level: JournalLevel) {
297 if let Ok(mut t) = self.levels.lock() {
298 t.set_all(level);
299 }
300 }
301
302 fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
303 if !self.is_accepted(category, level) {
304 return;
305 }
306 if let Ok(mut buf) = self.buffer.lock() {
307 buf.push_str(s);
308 }
309 }
310
311 fn flush(&self) {}
312}
313
314#[derive(Default)]
317pub struct Journalist {
318 journals: RefCell<Vec<Arc<dyn Journal>>>,
319}
320
321impl Journalist {
322 pub fn new() -> Self {
323 Self::default()
324 }
325
326 pub fn add_journal(&self, j: Arc<dyn Journal>) -> bool {
327 match self.journals.try_borrow_mut() {
328 Ok(journals) => {
329 for existing in journals.iter() {
330 if existing.name() == j.name() {
331 return false;
332 }
333 }
334 drop(journals);
335 self.journals.borrow_mut().push(j);
336 true
337 }
338 _ => false,
339 }
340 }
341
342 pub fn add_file_journal(
344 &self,
345 location_name: &str,
346 fname: &str,
347 default_level: JournalLevel,
348 append: bool,
349 ) -> Option<Arc<FileJournal>> {
350 let j = Arc::new(FileJournal::new(location_name, default_level));
351 if !j.open(fname, append) {
352 return None;
353 }
354 let dyn_j: Arc<dyn Journal> = j.clone();
355 if !self.add_journal(dyn_j) {
356 return None;
357 }
358 Some(j)
359 }
360
361 pub fn get_journal(&self, location_name: &str) -> Option<Arc<dyn Journal>> {
362 self.journals
363 .borrow()
364 .iter()
365 .find(|j| j.name() == location_name)
366 .cloned()
367 }
368
369 pub fn delete_all_journals(&self) {
370 self.journals.borrow_mut().clear();
371 }
372
373 pub fn print(&self, level: JournalLevel, category: JournalCategory, s: &str) {
377 for j in self.journals.borrow().iter() {
378 j.print(category, level, s);
379 }
380 }
381
382 pub fn print_indented(
384 &self,
385 level: JournalLevel,
386 category: JournalCategory,
387 indent_level: Index,
388 s: &str,
389 ) {
390 let pad = " ".repeat((indent_level.max(0) as usize) * 2);
391 let mut out = String::with_capacity(s.len() + pad.len());
393 let mut first = true;
394 for line in s.split_inclusive('\n') {
395 if !first || !line.is_empty() {
396 out.push_str(&pad);
397 }
398 out.push_str(line);
399 first = false;
400 }
401 self.print(level, category, &out);
402 }
403
404 pub fn produce_output(&self, level: JournalLevel, category: JournalCategory) -> bool {
406 self.journals
407 .borrow()
408 .iter()
409 .any(|j| j.is_accepted(category, level))
410 }
411
412 pub fn flush_buffer(&self) {
413 for j in self.journals.borrow().iter() {
414 j.flush();
415 }
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn level_filtering() {
425 let jnlst = Journalist::new();
426 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
427 jnlst.add_journal(j.clone());
428 jnlst.print(JournalLevel::J_ERROR, JournalCategory::J_MAIN, "err\n");
429 jnlst.print(
430 JournalLevel::J_DETAILED,
431 JournalCategory::J_MAIN,
432 "detail\n",
433 );
434 let s = j.contents();
435 assert!(s.contains("err"));
436 assert!(!s.contains("detail"));
437 }
438
439 #[test]
440 fn insuppressible_always_emits() {
441 let jnlst = Journalist::new();
442 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_NONE));
443 jnlst.add_journal(j.clone());
444 jnlst.print(
445 JournalLevel::J_INSUPPRESSIBLE,
446 JournalCategory::J_MAIN,
447 "x\n",
448 );
449 assert_eq!(j.contents(), "x\n");
450 }
451
452 #[test]
453 fn produce_output_reflects_journals() {
454 let jnlst = Journalist::new();
455 assert!(!jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
456 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
457 jnlst.add_journal(j);
458 assert!(jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
459 assert!(!jnlst.produce_output(JournalLevel::J_DETAILED, JournalCategory::J_MAIN));
460 }
461
462 #[test]
463 fn indent_prepends_two_spaces_per_level() {
464 let jnlst = Journalist::new();
465 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_ALL));
466 jnlst.add_journal(j.clone());
467 jnlst.print_indented(JournalLevel::J_SUMMARY, JournalCategory::J_MAIN, 2, "x\n");
468 assert_eq!(j.contents(), " x\n");
469 }
470}