use crate::types::Index;
use std::cell::RefCell;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(i32)]
#[allow(non_camel_case_types)]
pub enum JournalLevel {
J_INSUPPRESSIBLE = -1,
J_NONE = 0,
J_ERROR = 1,
J_STRONGWARNING = 2,
J_SUMMARY = 3,
J_WARNING = 4,
J_ITERSUMMARY = 5,
J_DETAILED = 6,
J_MOREDETAILED = 7,
J_VECTOR = 8,
J_MOREVECTOR = 9,
J_MATRIX = 10,
J_MOREMATRIX = 11,
J_ALL = 12,
}
impl JournalLevel {
pub const J_LAST_LEVEL: i32 = 13;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(usize)]
#[allow(non_camel_case_types)]
pub enum JournalCategory {
J_DBG = 0,
J_STATISTICS = 1,
J_MAIN = 2,
J_INITIALIZATION = 3,
J_BARRIER_UPDATE = 4,
J_SOLVE_PD_SYSTEM = 5,
J_FRAC_TO_BOUND = 6,
J_LINEAR_ALGEBRA = 7,
J_LINE_SEARCH = 8,
J_HESSIAN_APPROXIMATION = 9,
J_SOLUTION = 10,
J_DOCUMENTATION = 11,
J_NLP = 12,
J_TIMING_STATISTICS = 13,
J_USER_APPLICATION = 14,
J_USER1 = 15,
J_USER2 = 16,
J_USER3 = 17,
J_USER4 = 18,
J_USER5 = 19,
J_USER6 = 20,
J_USER7 = 21,
J_USER8 = 22,
J_USER9 = 23,
J_USER10 = 24,
J_USER11 = 25,
J_USER12 = 26,
J_USER13 = 27,
J_USER14 = 28,
J_USER15 = 29,
J_USER16 = 30,
J_USER17 = 31,
}
impl JournalCategory {
pub const J_LAST_CATEGORY: usize = 32;
}
pub trait Journal: Send + Sync {
fn name(&self) -> &str;
fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool;
fn set_print_level(&self, category: JournalCategory, level: JournalLevel);
fn set_all_print_levels(&self, level: JournalLevel);
fn print(&self, category: JournalCategory, level: JournalLevel, s: &str);
fn flush(&self);
}
struct LevelTable {
levels: [i32; JournalCategory::J_LAST_CATEGORY],
}
impl LevelTable {
fn new(default_level: JournalLevel) -> Self {
Self {
levels: [default_level as i32; JournalCategory::J_LAST_CATEGORY],
}
}
fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
if (level as i32) == JournalLevel::J_INSUPPRESSIBLE as i32 {
return true;
}
(level as i32) <= self.levels[category as usize]
}
fn set_level(&mut self, category: JournalCategory, level: JournalLevel) {
self.levels[category as usize] = level as i32;
}
fn set_all(&mut self, level: JournalLevel) {
for v in &mut self.levels {
*v = level as i32;
}
}
}
enum FileSink {
Stdout,
Stderr,
File(File),
}
impl FileSink {
fn write(&mut self, s: &str) -> io::Result<()> {
match self {
FileSink::Stdout => io::stdout().write_all(s.as_bytes()),
FileSink::Stderr => io::stderr().write_all(s.as_bytes()),
FileSink::File(f) => f.write_all(s.as_bytes()),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
FileSink::Stdout => io::stdout().flush(),
FileSink::Stderr => io::stderr().flush(),
FileSink::File(f) => f.flush(),
}
}
}
pub struct FileJournal {
name: String,
levels: Mutex<LevelTable>,
sink: Mutex<FileSink>,
}
impl FileJournal {
pub fn new(name: impl Into<String>, default_level: JournalLevel) -> Self {
Self {
name: name.into(),
levels: Mutex::new(LevelTable::new(default_level)),
sink: Mutex::new(FileSink::Stdout),
}
}
pub fn open(&self, fname: &str, append: bool) -> bool {
let new_sink = match fname {
"stdout" => FileSink::Stdout,
"stderr" => FileSink::Stderr,
other => {
let mut opts = OpenOptions::new();
opts.write(true).create(true);
if append {
opts.append(true);
} else {
opts.truncate(true);
}
match opts.open(other) {
Ok(f) => FileSink::File(f),
Err(_) => return false,
}
}
};
if let Ok(mut s) = self.sink.lock() {
*s = new_sink;
true
} else {
false
}
}
}
impl Journal for FileJournal {
fn name(&self) -> &str {
&self.name
}
fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
self.levels
.lock()
.map(|t| t.is_accepted(category, level))
.unwrap_or(false)
}
fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
if let Ok(mut t) = self.levels.lock() {
t.set_level(category, level);
}
}
fn set_all_print_levels(&self, level: JournalLevel) {
if let Ok(mut t) = self.levels.lock() {
t.set_all(level);
}
}
fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
if !self.is_accepted(category, level) {
return;
}
if let Ok(mut sink) = self.sink.lock() {
let _ = sink.write(s);
}
}
fn flush(&self) {
if let Ok(mut sink) = self.sink.lock() {
let _ = sink.flush();
}
}
}
pub struct StringJournal {
name: String,
levels: Mutex<LevelTable>,
buffer: Mutex<String>,
}
impl StringJournal {
pub fn new(name: impl Into<String>, default_level: JournalLevel) -> Self {
Self {
name: name.into(),
levels: Mutex::new(LevelTable::new(default_level)),
buffer: Mutex::new(String::new()),
}
}
pub fn contents(&self) -> String {
self.buffer.lock().map(|b| b.clone()).unwrap_or_default()
}
pub fn take(&self) -> String {
self.buffer
.lock()
.map(|mut b| std::mem::take(&mut *b))
.unwrap_or_default()
}
}
impl Journal for StringJournal {
fn name(&self) -> &str {
&self.name
}
fn is_accepted(&self, category: JournalCategory, level: JournalLevel) -> bool {
self.levels
.lock()
.map(|t| t.is_accepted(category, level))
.unwrap_or(false)
}
fn set_print_level(&self, category: JournalCategory, level: JournalLevel) {
if let Ok(mut t) = self.levels.lock() {
t.set_level(category, level);
}
}
fn set_all_print_levels(&self, level: JournalLevel) {
if let Ok(mut t) = self.levels.lock() {
t.set_all(level);
}
}
fn print(&self, category: JournalCategory, level: JournalLevel, s: &str) {
if !self.is_accepted(category, level) {
return;
}
if let Ok(mut buf) = self.buffer.lock() {
buf.push_str(s);
}
}
fn flush(&self) {}
}
#[derive(Default)]
pub struct Journalist {
journals: RefCell<Vec<Arc<dyn Journal>>>,
}
impl Journalist {
pub fn new() -> Self {
Self::default()
}
pub fn add_journal(&self, j: Arc<dyn Journal>) -> bool {
if let Ok(journals) = self.journals.try_borrow_mut() {
for existing in journals.iter() {
if existing.name() == j.name() {
return false;
}
}
drop(journals);
self.journals.borrow_mut().push(j);
true
} else {
false
}
}
pub fn add_file_journal(
&self,
location_name: &str,
fname: &str,
default_level: JournalLevel,
append: bool,
) -> Option<Arc<FileJournal>> {
let j = Arc::new(FileJournal::new(location_name, default_level));
if !j.open(fname, append) {
return None;
}
let dyn_j: Arc<dyn Journal> = j.clone();
if !self.add_journal(dyn_j) {
return None;
}
Some(j)
}
pub fn get_journal(&self, location_name: &str) -> Option<Arc<dyn Journal>> {
self.journals
.borrow()
.iter()
.find(|j| j.name() == location_name)
.cloned()
}
pub fn delete_all_journals(&self) {
self.journals.borrow_mut().clear();
}
pub fn print(&self, level: JournalLevel, category: JournalCategory, s: &str) {
for j in self.journals.borrow().iter() {
j.print(category, level, s);
}
}
pub fn print_indented(
&self,
level: JournalLevel,
category: JournalCategory,
indent_level: Index,
s: &str,
) {
let pad = " ".repeat((indent_level.max(0) as usize) * 2);
let mut out = String::with_capacity(s.len() + pad.len());
let mut first = true;
for line in s.split_inclusive('\n') {
if !first || !line.is_empty() {
out.push_str(&pad);
}
out.push_str(line);
first = false;
}
self.print(level, category, &out);
}
pub fn produce_output(&self, level: JournalLevel, category: JournalCategory) -> bool {
self.journals
.borrow()
.iter()
.any(|j| j.is_accepted(category, level))
}
pub fn flush_buffer(&self) {
for j in self.journals.borrow().iter() {
j.flush();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_filtering() {
let jnlst = Journalist::new();
let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
jnlst.add_journal(j.clone());
jnlst.print(JournalLevel::J_ERROR, JournalCategory::J_MAIN, "err\n");
jnlst.print(
JournalLevel::J_DETAILED,
JournalCategory::J_MAIN,
"detail\n",
);
let s = j.contents();
assert!(s.contains("err"));
assert!(!s.contains("detail"));
}
#[test]
fn insuppressible_always_emits() {
let jnlst = Journalist::new();
let j = Arc::new(StringJournal::new("buf", JournalLevel::J_NONE));
jnlst.add_journal(j.clone());
jnlst.print(
JournalLevel::J_INSUPPRESSIBLE,
JournalCategory::J_MAIN,
"x\n",
);
assert_eq!(j.contents(), "x\n");
}
#[test]
fn produce_output_reflects_journals() {
let jnlst = Journalist::new();
assert!(!jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
let j = Arc::new(StringJournal::new("buf", JournalLevel::J_SUMMARY));
jnlst.add_journal(j);
assert!(jnlst.produce_output(JournalLevel::J_ERROR, JournalCategory::J_MAIN));
assert!(!jnlst.produce_output(JournalLevel::J_DETAILED, JournalCategory::J_MAIN));
}
#[test]
fn indent_prepends_two_spaces_per_level() {
let jnlst = Journalist::new();
let j = Arc::new(StringJournal::new("buf", JournalLevel::J_ALL));
jnlst.add_journal(j.clone());
jnlst.print_indented(JournalLevel::J_SUMMARY, JournalCategory::J_MAIN, 2, "x\n");
assert_eq!(j.contents(), " x\n");
}
}