diff --git a/Cargo.toml b/Cargo.toml index 8d023fd..d645556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ +workspace = { members = ["config", "logging"] } + [package] name = "wanessa" version = "0.1.0" @@ -9,12 +11,13 @@ edition = "2021" [lints.rust] unsafe_code = "forbid" +missing_docs = "warn" [lints.clippy] enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery -unwrap_used = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used +unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn -missing_assert_message = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message -missing_errors_doc = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message \ No newline at end of file +missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message +missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message diff --git a/config/Cargo.toml b/config/Cargo.toml new file mode 100644 index 0000000..4e5c462 --- /dev/null +++ b/config/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "config" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +getset = { version = "0.1.2", default-features = false } +once_cell = { version = "1.19.0", default-features = false, features = ["std"] } + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[lints.clippy] +enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use +pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic +nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery +unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used +missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn +missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message +missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message diff --git a/config/src/db.rs b/config/src/db.rs new file mode 100644 index 0000000..ad74bf2 --- /dev/null +++ b/config/src/db.rs @@ -0,0 +1,57 @@ +/*! +* This module contains everything related to [`DbConfig`]. +*/ +#![allow(clippy::module_name_repetitions)] + +use getset::Getters; + +/** +* A immutable record of all the information needed to connect to the SQL database. +*/ +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, PartialEq, Eq, Getters, Debug)] +#[getset(get = "pub")] +pub struct DbConfig { + /// Database connection address. + /// Is an option to allow constructing a default config during compile time.
+ addr: String, + /// Database connection port.
+ port: u16, +} + +impl Default for DbConfig { + fn default() -> Self { + Self { + addr: String::from("localhost"), + port: 6969, + } + } +} + +/** +* Builder for [`DbConfig`]. +*/ +#[derive(Default, Debug)] +pub struct DbConfigBuilder(DbConfig); + +impl DbConfigBuilder { + /// Get a new [`DbConfigBuilder`] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Set the address to the location of the database. + #[must_use] + pub fn set_addr(mut self, addr: impl Into) -> Self { + self.0.addr = addr.into(); + self + } + + /// Set the port to the port the database uses. + #[must_use] + pub fn set_port(mut self, port: impl Into) -> Self { + self.0.port = port.into(); + self + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs new file mode 100644 index 0000000..4534cfe --- /dev/null +++ b/config/src/lib.rs @@ -0,0 +1,31 @@ +/*! +* Containing all singleton and thread-safe structs related to configuring `WANessa`. +* +* This crate differentiates between `Configs` and `Settings`: +* Configs: +* - Configs are immutable after they have been initialized. Example [`DbConfig`]. +* - Changing the config requires a restart of `WANessa`. +* +* Settings: +* - Settings are mutable after they have been initialized. Example [`LogSettings`]. +* - `WANessa` always uses the current setting and does not need a restart to apply new settings. +*/ + +pub mod db; +pub mod log; + +use crate::db::DbConfig; +use crate::log::LogSettings; + +use std::sync::{Arc, RwLock}; + +use once_cell::sync::Lazy; + +// TODO: replace default with parsed settings from files+flags + +/// Singelton [`DbConfig`]. +pub static DB_CONFIG: Lazy> = Lazy::new(|| Arc::new(DbConfig::default())); + +/// Singelton [`LogSettings`]. +pub static LOG_SETTINGS: Lazy> = + Lazy::new(|| RwLock::new(LogSettings::default())); diff --git a/config/src/log.rs b/config/src/log.rs new file mode 100644 index 0000000..87f2c4e --- /dev/null +++ b/config/src/log.rs @@ -0,0 +1,98 @@ +/*! +* This module contains everything related to [`LogSettings`]. +*/ +#![allow(clippy::module_name_repetitions)] +use std::cmp::Ordering; +use std::path::Path; + +use crate::log::LogVerbosity::Warning; + +use std::path::PathBuf; + +use getset::Getters; +use getset::Setters; + +/** +* All settings relating to how the project logs information. +*/ +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, PartialEq, Eq, Getters, Setters, Debug)] +#[getset(get = "pub", set = "pub")] +pub struct LogSettings { + /// See [`LogVerbosity`].
+ verbosity: LogVerbosity, + /// Logs UTC time and date of message, if true.
+ time: bool, + /// Time and date format.
+ /// See [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
+ time_format: String, + /// Logs location in code, where the message was logged, if true.
+ location: bool, + /// If `Some(path)` tries to also write the log to `path` in addition to stderr/stderr.
+ #[getset(skip)] + path: Option, + /// Logs to standard out, if true.
+ stdout: bool, + /// Logs to standard err, if true.
+ stderr: bool, +} + +impl LogSettings { + /// Setter for log path, including syntactic sugar for the [Option] enum. + pub fn set_path(&mut self, path: impl Into>) { + self.path = path.into(); + } + + /// Getter for the log path. + #[must_use] + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() + } +} + +impl Default for LogSettings { + fn default() -> Self { + Self { + verbosity: Warning, + time: false, + time_format: String::from("%F-%T:%f"), + location: false, + stdout: true, + stderr: true, + path: None, + } + } +} + +/** +* Each level includes the previous ones. +*/ +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum LogVerbosity { + /// Critical Errors, may lead to crashes or deactivating certain features. + Error = 10, + /// Very minor and recovered errors, such as invalid configs. + Warning = 20, + /// Very verbose and detailed. Basically gives a step-by-step instruction on what is currently done. + Information = 30, + /// Very technical and even more verbose. + /// May contain secrets and private information. + /// **Do not use in production environments!** + Debugging = 40, +} + +impl PartialOrd for LogVerbosity { + /// Some operator overloading of comparison symbols (==, <,>=, etc.) as syntactic sugar. + /// See [`PartialOrd`]. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for LogVerbosity { + /// Some operator overloading of comparison symbols (==, <,>=, etc.) as syntactic sugar. + /// See [`Ord`]. + fn cmp(&self, other: &Self) -> Ordering { + (*self as usize).cmp(&(*other as usize)) + } +} diff --git a/logging/Cargo.toml b/logging/Cargo.toml new file mode 100644 index 0000000..99b9e87 --- /dev/null +++ b/logging/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "logging" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4.34", default-features = false, features = ["now"] } +config = { path = "../config"} + +[dev-dependencies] +uuid = { version = "1.7.0", default-features = false, features = ["v4", "fast-rng"] } +once_cell = { version = "1.19.0", default-features = false, features = ["std"] } +futures = { version = "0.3.30", default-features = false, features = ["alloc", "executor"] } + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[lints.clippy] +enum_glob_use = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/enum_glob_use +pedantic = "deny" # https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=pedantic +nursery = "deny" # wip lints: https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=nursery +unwrap_used = "forbid" # https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used +missing_const_for_fn = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_const_for_fn +missing_assert_message = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message +missing_errors_doc = "warn" # https://rust-lang.github.io/rust-clippy/master/index.html#/missing_assert_message diff --git a/logging/src/lib.rs b/logging/src/lib.rs new file mode 100644 index 0000000..d73bebf --- /dev/null +++ b/logging/src/lib.rs @@ -0,0 +1,195 @@ +/*! +* This module handles logging messages asynchronously and is thread-safe. +* It should mostly be called statically using the [`log!`] macro. +*/ + +mod test; + +use std::fmt::Display; +use std::fs::OpenOptions; +use std::io; +use std::io::Write; + +use config::log::LogSettings; +use config::log::LogVerbosity; + +use chrono::Utc; + +use LogVerbosity::Information; +use LogVerbosity::Warning; + +use LogMessageType::GenericDebug; +use LogMessageType::GenericErr; +use LogMessageType::GenericInfo; +use LogMessageType::GenericWarn; + +/** +* Logs the given message. +*/ +pub fn log_message( + msg: &LogMessage, + conf: &LogSettings, + file: &str, + line: u32, + column: u32, +) -> Result<(), io::Error> { + // Check if message may be logged according to config. + let Some(log_line) = log_to_str(msg, conf, file, line, column) else { + return Ok(()); + }; + + // Log to file + match conf.path().as_ref() { + None => { /* Do not log to file */ } + Some(p) => { + let file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(p); + let mut file = match file { + Ok(f) => f, + Err(e) => return Err(e), + }; + match writeln!(file, "{log_line}") { + Ok(_) => {} + Err(e) => return Err(e), + } + } + }; + + if msg.1 <= Warning && *conf.stderr() { + let mut stdout = io::stdout().lock(); + return writeln!(stdout, "{log_line}"); + } else if msg.1 >= Information && *conf.stdout() { + let mut stderr = io::stderr().lock(); + return writeln!(stderr, "{log_line}"); + } + return Ok(()); +} + +/** +* Return log line, if message may be logged according to [`config::log::LogSettings`]. +*/ +#[must_use] +pub fn log_to_str( + msg: &LogMessage, + conf: &LogSettings, + file: &str, + line: u32, + column: u32, +) -> Option { + if conf.verbosity() < &msg.1 { + return None; + } + + let mut log_line = String::new(); + + // add time substring + if *conf.time() { + log_line += &format!("{} ", Utc::now().format(conf.time_format())); + } + + // add code location substring + if *conf.location() { + log_line += &format!("{file}:{line},{column} "); + } + + Some(log_line + &msg.to_string()) +} + +/** +* Shorthand version for [`log_message`], which does not require information about where in the code +* the command was called from. +* Tries to aqcuire a read lock, if a reference to an instance of [`config::log::LogSettings`] is +* not given. +*/ +#[macro_export] +macro_rules! log { + ($msg:expr) => { + let conf = config::LOG_SETTINGS + .read() + .unwrap_or_else(|_| panic!("Failed aqcuire read lock on config!")); + let res = log_message($msg, &*conf, file!(), line!(), column!()); + drop(conf); + res + }; + ($msg:expr, $config:expr) => { + log_message($msg, $config, file!(), line!(), column!()) + }; +} + +/** +* A named typle assigning a log [`LogVerbosity`] to a [`LogMessageType`]. +*/ +#[derive(PartialEq, Eq, Debug)] +pub struct LogMessage(LogMessageType, LogVerbosity); + +impl Display for LogMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + self.0.fmt(f) // just display LogMessageType + } +} + +// TODO: Evaluate replacing the following with a trait, to decouple this component from the rest. + +/** +* Every possible Message, which may be logged. Grouped the following: +* +* These groups correspond to the four levels of logging verbosity. See [`LogVerbosity`]. +*/ +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LogMessageType { + /// Errors + /// An error of any type; please avoid using this. + GenericErr(String), + /// Warnings + /// A warning of any type; please avoid using this. + GenericWarn(String), + /// Info + /// Some information of any type; please avoid using this. + GenericInfo(String), + /// Debug + /// a debug message of any type; please avoid using this. + GenericDebug(String), +} + +/* +* Please don't put anything other except new match arms below this comment, since it is expected that +* the following code will have a lot of lines, due to exhausting all possible enum values +* respectively and (hopefully) detailed messages. +*/ + +impl LogMessageType { + /// Returns a new [`LogMessage`] based on the default verbosity of the given [`LogMessageType`]. + #[must_use] + pub fn log_message(&self) -> LogMessage { + let verbosity = match self { + GenericErr(_) => LogVerbosity::Error, + GenericWarn(_) => LogVerbosity::Warning, + GenericInfo(_) => LogVerbosity::Information, + GenericDebug(_) => LogVerbosity::Debugging, + }; + + LogMessage(self.clone(), verbosity) + } +} + +impl Display for LogMessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + GenericErr(err) => { + write!(f, "Generic Error: {err}") + } + GenericWarn(warn) => { + write!(f, "Generic Warning: {warn}") + } + GenericInfo(info) => { + write!(f, "Generic Information: {info}") + } + GenericDebug(debug) => { + write!(f, "Generic Debug Message: {debug}") + } + } + } +} diff --git a/logging/src/test.rs b/logging/src/test.rs new file mode 100644 index 0000000..8293a72 --- /dev/null +++ b/logging/src/test.rs @@ -0,0 +1,303 @@ +#![cfg(test)] +/*! +* This test suite uses uuid to easily avoid race conditions when writing to the same log file. +*/ + +use std::{ + collections::HashSet, + env, + fs::{create_dir_all, read_to_string}, + mem::take, + path::PathBuf, +}; + +use futures::{executor::block_on, future::join_all}; +use once_cell::sync::Lazy; + +use uuid::Uuid; + +use super::*; + +const CONCURRENT_MESSAGE_COUNT: usize = 99999; + +static LOG_DIR: Lazy = Lazy::new(|| { + if cfg!(unix) { + String::from("/tmp/WANessa/unit-tests/logging") + } else if cfg!(windows) { + let tmp_path = env::var("TMP").unwrap_or_else(|_| { + env::var("TEMP").expect( + "Windows should have both TMP and TEMP, but you have neither, what did you do?", + ) + }); + format!("{tmp_path}/WANessa/unit-tests/logging") + } else { + String::new() + } +}); + +/// Tests if the macro logs properly with a given config. +#[test] +pub fn log_macro_given_config() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message(); + let mut config = config::log::LogSettings::default(); + config.set_path(PathBuf::from(log_path)); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + log!(&message, &config); + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(message.to_string() + "\n", log_file); +} + +/// Tests if [`log_message`] to file correctly. +#[test] +pub fn log_msg_file() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message(); + let mut config = config::log::LogSettings::default(); + config.set_path(PathBuf::from(log_path)); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + log!(&message, &config); + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(message.to_string() + "\n", log_file); +} + +/// Tests if [`log_message`] does not modify output from [`log_to_str`], when logging to file. +#[test] +pub fn log_str() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message(); + let mut config = config::log::LogSettings::default(); + config.set_path(PathBuf::from(log_path)); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + let log_line = log_to_str(&message, &config, file!(), line!(), column!()) + .expect("There should be a log line.") + + "\n"; + + log!(&message, &config); + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(log_line, log_file); +} + +/// Tests if no messages are unintentionally filtered due to their verboisity. +#[test] +pub fn verbosity_no_filter() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let messages = vec![ + LogMessageType::GenericErr(String::from("Test Err")).log_message(), + LogMessageType::GenericWarn(String::from("Test Warn")).log_message(), + LogMessageType::GenericInfo(String::from("Test Info")).log_message(), + LogMessageType::GenericDebug(String::from("Test Debug")).log_message(), + ]; + let mut config = config::log::LogSettings::default(); + config.set_path(PathBuf::from(log_path)); + config.set_verbosity(LogVerbosity::Error); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + let log_line = log_to_str(&messages[0], &config, file!(), line!(), column!()) + .expect("There should be a log line.") + + "\n"; + + for msg in messages { + log!(&msg, &config); + } + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(log_file, log_line); +} + +/// Tests if messages are properly filtered according to their verbosity. +#[test] +pub fn verbosity_filter() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let messages = vec![ + LogMessageType::GenericErr(String::from("Test Err")).log_message(), + LogMessageType::GenericWarn(String::from("Test Warn")).log_message(), + LogMessageType::GenericInfo(String::from("Test Info")).log_message(), + LogMessageType::GenericDebug(String::from("Test Debug")).log_message(), + ]; + let mut config = config::log::LogSettings::default(); + config.set_path(PathBuf::from(log_path)); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + let mut log_line = log_to_str(&messages[0], &config, file!(), line!(), column!()) + .expect("There should be a log line.") + + "\n"; + log_line += &(log_to_str(&messages[1], &config, file!(), line!(), column!()) + .expect("There should be a log line.") + + "\n"); + + for msg in messages { + log!(&msg, &config); + } + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(log_file, log_line); +} + +/** +* All testing concurrency in a controlled manner. +* +* When a test modifies the config according to it's requirements, another test might panic, because +* it might read the result from the changed config. +*/ +#[test] +pub fn concurrency_tests() { + log_macro_shared_config(); + log_concurrently_any_order(); + log_concurrently_correct_order(); +} + +/** +* Tests if log macro logs to file correctly. +* [`config::CONFIG`] +*/ +fn log_macro_shared_config() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let message = LogMessageType::GenericWarn(String::from("Test Log")).log_message(); + let mut config = config::LOG_SETTINGS + .write() + .expect("Could not acquire write lock on config!"); + take(&mut *config); + config.set_path(PathBuf::from(log_path)); + config.set_stdout(false); + config.set_stderr(false); + drop(config); + + create_dir_all(PathBuf::from(LOG_DIR.as_str())) + .unwrap_or_else(|_| panic!("Could not create directory: {}", *LOG_DIR)); + + log!(&message); + + let log_file = read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")); + + assert_eq!(message.to_string() + "\n", log_file); +} + +/** +* Tests concurrent logging. Log lines may be in any order. +*/ +fn log_concurrently_any_order() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let mut config = config::LOG_SETTINGS + .write() + .expect("Could not acquire write lock on config!"); + take(&mut *config); + let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT); + config.set_path(PathBuf::from(log_path)); + config.set_stdout(false); + config.set_stderr(false); + drop(config); + + for i in 0..CONCURRENT_MESSAGE_COUNT { + let msg = i.to_string(); + messages.push(async { + log!(&LogMessageType::GenericWarn(msg).log_message()); + }); + } + + block_on(join_all(messages)); + + let mut num_set = HashSet::with_capacity(CONCURRENT_MESSAGE_COUNT); + for i in 0..CONCURRENT_MESSAGE_COUNT { + num_set.insert(i); + } + + for line in read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")) + .lines() + { + let num_str = line + .split_whitespace() + .last() + .unwrap_or_else(|| panic!("Could not get message number from line: {line}")); + let num = num_str + .parse() + .unwrap_or_else(|_| panic!("Could not parse number: {num_str}")); + assert!(num_set.remove(&num)); + } + + assert_eq!(num_set.len(), 0); +} + +/** +* Tests concurrent logging. Log lines must be in order. +*/ +fn log_concurrently_correct_order() { + let log_path = &format!("{}/{}", *LOG_DIR, Uuid::new_v4()); + println!("Log Path: {log_path}"); + let mut config = config::LOG_SETTINGS + .write() + .expect("Could not acquire write lock on config!"); + take(&mut *config); + let mut messages = Vec::with_capacity(CONCURRENT_MESSAGE_COUNT); + config.set_path(PathBuf::from(log_path)); + config.set_stdout(false); + config.set_stderr(false); + drop(config); + + for i in 0..CONCURRENT_MESSAGE_COUNT { + let msg = i.to_string(); + messages.push(async { + log!(&LogMessageType::GenericWarn(msg).log_message()); + }); + } + + block_on(join_all(messages)); + + let mut num_set = HashSet::with_capacity(CONCURRENT_MESSAGE_COUNT); + for i in 0..CONCURRENT_MESSAGE_COUNT { + num_set.insert(i); + } + + for (i, line) in read_to_string(PathBuf::from(log_path)) + .unwrap_or_else(|_| panic!("Could not read file: {log_path}")) + .lines() + .enumerate() + { + let num_str = line + .split_whitespace() + .last() + .unwrap_or_else(|| panic!("Could not get message number from line: {line}")); + let num = num_str + .parse() + .unwrap_or_else(|_| panic!("Could not parse number: {num_str}")); + assert!(num_set.remove(&num)); + assert_eq!(i, num); + } + + assert_eq!(num_set.len(), 0); +}