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 if let Ok(mut s) = self.sink.lock() {
201 *s = new_sink;
202 true
203 } else {
204 false
205 }
206 }
207}
208
209impl Journal for FileJournal {
210 fn name(&self) -> &str {
211 &self.name
212 }
213
214 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
215 self.levels
216 .lock()
217 .map(|t| t.is_accepted(category, level))
218 .unwrap_or(false)
219 }
220
221 fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
222 if let Ok(mut t) = self.levels.lock() {
223 t.set_level(category, level);
224 }
225 }
226
227 fn set_all_print_levels(&self, level: JournalLevel) {
228 if let Ok(mut t) = self.levels.lock() {
229 t.set_all(level);
230 }
231 }
232
233 fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
234 if !self.is_accepted(category, level) {
235 return;
236 }
237 if let Ok(mut sink) = self.sink.lock() {
238 let _ = sink.write(s);
239 }
240 }
241
242 fn flush(&self) {
243 if let Ok(mut sink) = self.sink.lock() {
244 let _ = sink.flush();
245 }
246 }
247}
248
249pub struct StringJournal {
251 name: String,
252 levels: Mutex<LevelTable>,
253 buffer: Mutex<String>,
254}
255
256impl StringJournal {
257 pub fn new(name: impl Into<String>, default_level: JournalLevel) -> Self {
258 Self {
259 name: name.into(),
260 levels: Mutex::new(LevelTable::new(default_level)),
261 buffer: Mutex::new(String::new()),
262 }
263 }
264
265 pub fn contents(&self) -> String {
266 self.buffer.lock().map(|b| b.clone()).unwrap_or_default()
267 }
268
269 pub fn take(&self) -> String {
270 self.buffer
271 .lock()
272 .map(|mut b| std::mem::take(&mut *b))
273 .unwrap_or_default()
274 }
275}
276
277impl Journal for StringJournal {
278 fn name(&self) -> &str {
279 &self.name
280 }
281
282 fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
283 self.levels
284 .lock()
285 .map(|t| t.is_accepted(category, level))
286 .unwrap_or(false)
287 }
288
289 fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
290 if let Ok(mut t) = self.levels.lock() {
291 t.set_level(category, level);
292 }
293 }
294
295 fn set_all_print_levels(&self, level: JournalLevel) {
296 if let Ok(mut t) = self.levels.lock() {
297 t.set_all(level);
298 }
299 }
300
301 fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
302 if !self.is_accepted(category, level) {
303 return;
304 }
305 if let Ok(mut buf) = self.buffer.lock() {
306 buf.push_str(s);
307 }
308 }
309
310 fn flush(&self) {}
311}
312
313#[derive(Default)]
316pub struct Journalist {
317 journals: RefCell<Vec<Arc<dyn Journal>>>,
318}
319
320impl Journalist {
321 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn add_journal(&self, j: Arc<dyn Journal>) -> bool {
326 if let Ok(journals) = self.journals.try_borrow_mut() {
327 for existing in journals.iter() {
328 if existing.name() == j.name() {
329 return false;
330 }
331 }
332 drop(journals);
333 self.journals.borrow_mut().push(j);
334 true
335 } else {
336 false
337 }
338 }
339
340 pub fn add_file_journal(
342 &self,
343 location_name: &str,
344 fname: &str,
345 default_level: JournalLevel,
346 append: bool,
347 ) -> Option<Arc<FileJournal>> {
348 let j = Arc::new(FileJournal::new(location_name, default_level));
349 if !j.open(fname, append) {
350 return None;
351 }
352 let dyn_j: Arc<dyn Journal> = j.clone();
353 if !self.add_journal(dyn_j) {
354 return None;
355 }
356 Some(j)
357 }
358
359 pub fn get_journal(&self, location_name: &str) -> Option<Arc<dyn Journal>> {
360 self.journals
361 .borrow()
362 .iter()
363 .find(|j| j.name() == location_name)
364 .cloned()
365 }
366
367 pub fn delete_all_journals(&self) {
368 self.journals.borrow_mut().clear();
369 }
370
371 pub fn print(&self, level: JournalLevel, category: JournalCategory, s: &str) {
375 for j in self.journals.borrow().iter() {
376 j.print(category, level, s);
377 }
378 }
379
380 pub fn print_indented(
382 &self,
383 level: JournalLevel,
384 category: JournalCategory,
385 indent_level: Index,
386 s: &str,
387 ) {
388 let pad = " ".repeat((indent_level.max(0) as usize) * 2);
389 let mut out = String::with_capacity(s.len() + pad.len());
391 let mut first = true;
392 for line in s.split_inclusive('\n') {
393 if !first || !line.is_empty() {
394 out.push_str(&pad);
395 }
396 out.push_str(line);
397 first = false;
398 }
399 self.print(level, category, &out);
400 }
401
402 pub fn produce_output(&self, level: JournalLevel, category: JournalCategory) -> bool {
404 self.journals
405 .borrow()
406 .iter()
407 .any(|j| j.is_accepted(category, level))
408 }
409
410 pub fn flush_buffer(&self) {
411 for j in self.journals.borrow().iter() {
412 j.flush();
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn level_filtering() {
423 let jnlst = Journalist::new();
424 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
425 jnlst.add_journal(j.clone());
426 jnlst.print(JournalLevel::J_ERROR, JournalCategory::J_MAIN, "err\n");
427 jnlst.print(
428 JournalLevel::J_DETAILED,
429 JournalCategory::J_MAIN,
430 "detail\n",
431 );
432 let s = j.contents();
433 assert!(s.contains("err"));
434 assert!(!s.contains("detail"));
435 }
436
437 #[test]
438 fn insuppressible_always_emits() {
439 let jnlst = Journalist::new();
440 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_NONE));
441 jnlst.add_journal(j.clone());
442 jnlst.print(
443 JournalLevel::J_INSUPPRESSIBLE,
444 JournalCategory::J_MAIN,
445 "x\n",
446 );
447 assert_eq!(j.contents(), "x\n");
448 }
449
450 #[test]
451 fn produce_output_reflects_journals() {
452 let jnlst = Journalist::new();
453 assert!(!jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
454 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
455 jnlst.add_journal(j);
456 assert!(jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
457 assert!(!jnlst.produce_output(JournalLevel::J_DETAILED, JournalCategory::J_MAIN));
458 }
459
460 #[test]
461 fn indent_prepends_two_spaces_per_level() {
462 let jnlst = Journalist::new();
463 let j = Arc::new(StringJournal::new("buf", JournalLevel::J_ALL));
464 jnlst.add_journal(j.clone());
465 jnlst.print_indented(JournalLevel::J_SUMMARY, JournalCategory::J_MAIN, 2, "x\n");
466 assert_eq!(j.contents(), " x\n");
467 }
468}