rust-utils 0.16.0

Various utility routines used in the rust programs I have written
Documentation
use crate as rust_utils;

#[cfg(test)]
use crate::{
    utils::{self, get_input},
    logging::*,
    config::*,
    config,
    hash_map,
    chainable,
    encapsulated,
    New
};
use std::{
    fs, env,
    thread,
    path::PathBuf
};

use serde::{Deserialize, Serialize};
use chrono::{Datelike, Local};
use regex::Regex;

#[cfg(feature = "futures")]
use crate::futures::exec_future;

// convenience function tests
#[test]
fn get_execname() {
    println!("get_execname() test:");
    println!("This test's executable name is {}", utils::get_execname());
}

#[test]
fn capitalize() {
    println!("capitalize() test:");
    let text = utils::capitalize("test");
    assert_eq!(text, "Test");
}

#[test]
fn get_parent() {
    println!("get_parent() test:");
    let home_dir = format!("{}/", env::var("HOME").unwrap());
    println!("{home_dir}");
    let parent = utils::get_parent(home_dir);
    assert_eq!(parent, "/home");
}

#[test]
fn get_filename() {
    println!("get_filename() test:");
    let dir = "/tmp/test.txt";
    fs::write("/tmp/test.txt", "test").unwrap();
    let file = utils::get_filename(dir);
    assert_eq!(file, "test.txt");
    fs::remove_file("/tmp/test.txt").unwrap();
}

#[test]
fn alphabetize() {
    println!("alphabetize() test:");

    // the alphabet, randomized
    const ALPHA_UPPER_EXPECTED: [&str; 26] = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
    const ALPHA_LOWER_SCRAMBLED: [&str; 26] = ["e" ,"c", "r", "n", "x", "j", "o", "a", "g", "v", "y", "f", "l", "s", "q", "d", "k", "t", "b", "w", "m", "h", "u", "i", "p", "z"];
    const ALPHA_LOWER_EXPECTED: [&str; 26] = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    const ALPHA_UPPER_SCRAMBLED: [&str; 26] = ["J", "B", "R", "L", "C", "P", "Y", "D", "F", "K", "A", "U", "O", "S", "Z", "E", "X", "Q", "I", "W", "T", "V", "H", "M", "N", "G"];

    // uppercase letters
    let mut alpha_vec: Vec<String> = utils::alphabetize(&ALPHA_UPPER_SCRAMBLED);

    // are the letters in the correct order?
    for (count,letter) in alpha_vec.iter().enumerate() {
        assert_eq!(letter, ALPHA_UPPER_EXPECTED[count]);
    }
    println!("Uppercase: {alpha_vec:?}\n");

    // lowercase letters
    alpha_vec = utils::alphabetize(&ALPHA_LOWER_SCRAMBLED);

    // are the letters in the correct order?
    for (count,letter) in alpha_vec.iter().enumerate() {
        assert_eq!(letter, ALPHA_LOWER_EXPECTED[count]);
    }
    println!("Lowercase: {alpha_vec:?}");
}

// logging API test
#[test]
fn logging_test() {
    let info_regex = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[INFO\]: Yes. Is this useful?").unwrap();
    let info_regex2 = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[INFO\]: Yes.").unwrap();
    let debug_regex = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[DEBUG\]: Debug info").unwrap();
    let debug_regex2 = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[DEBUG\]: This shouldn't show up").unwrap();
    let warn_regex = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[WARN\]: There may be an issue!").unwrap();
    let error_regex = Regex::new(r"\[[0-9]*:[0-9][0-9]:[0-9][0-9]\] \[ERROR\]: Houston, we have a problem!").unwrap();
    let expected_name = {
        let now = Local::now();
        format!("test-{}-{}-{}.log", now.year(), now.month(), now.day())
    };
    let path = format!("{}/.local/share/coof-testing/{expected_name}", env::var("HOME").expect("Where the hell is your home folder?!"));
    fs::remove_file(&path).unwrap_or(());
    let log = Log::new("test", "coof-testing");
    log.line(LogLevel::Info, "Yes. Is this useful?", true);
    log.line_basic("Yes.", true);
    log.line(LogLevel::Debug(true), "Debug info", true);
    log.line(LogLevel::Debug(false), "This shouldn't show up", true);
    log.line(LogLevel::Warn, "There may be an issue!", true);
    log.line(LogLevel::Error, "Houston, we have a problem!", true);
    let file = fs::read_to_string(&path).unwrap();
    assert!(info_regex.is_match(&file));
    assert!(info_regex2.is_match(&file));
    assert!(debug_regex.is_match(&file));
    assert!(!debug_regex2.is_match(&file));
    assert!(warn_regex.is_match(&file));
    assert!(error_regex.is_match(&file));

    let log2 = log.clone();
    let thread = thread::spawn(move || {
        for _ in 0..100 {
            log2.line(LogLevel::Info, "Concurrent logging...", true);
        }
    });

    for _ in 0..100 {
        log.line(LogLevel::Info, "Concurrent logging!!!", true);
    }

    thread.join().expect("thread did not complete successfully!");
}

#[should_panic]
#[test]
fn logging_panic_test() {
    let log = Log::new("test", "coof-testing");
    log.report_panics(true);
    let user = env::var("USER").expect("What is your name again?");
    panic!("{user} has tested positive for the coof!");
}

// config api test
#[test]
fn config_test() {
    #[derive(Deserialize, Serialize, PartialEq, Debug)]
    struct TestConfig {
        string: String,
        number: u32,
        boolean: bool
    }

    impl Default for TestConfig {
        fn default() -> TestConfig {
            TestConfig {
                string: "string".into(),
                number: 100,
                boolean: true
            }
        }
    }

    impl Config for TestConfig {
        const FILE_NAME: &'static str = "test.toml";

        fn get_save_dir() -> PathBuf {
            Self::get_config_root()
                .join("coof-testing")
        }
    }

    #[derive(Default, PartialEq, Debug)]
    #[config(save_dir = "coof-testing", file_name = "test2.toml", cfg_type(ron))]
    struct MacroConfig {
        name: String,
        id: usize,
        ok: bool
    }

    let config_dir = format!("{}/.config/coof-testing", env::var("HOME").expect("Where the hell is your home folder?!"));
    fs::remove_dir_all(config_dir).unwrap_or(());
    let expected = TestConfig::default();
    let mut config = TestConfig::load();

    assert_eq!(expected, config);

    config.number = 5;
    config.save().expect("Unable to save config!");

    let expected = TestConfig {
        string: "string".to_string(),
        number: 5,
        boolean: true
    };

    let config = TestConfig::load();
    assert_eq!(expected, config);

    let macro_config = MacroConfig::load();
    macro_config.save().expect("Unable to save config!");
}

// command executor test
#[test]
fn run_cmd_test() {
    let reg_cmd = utils::run_command("nmcli", false, ["networking", "connectivity", "check"]);
    let priv_cmd = utils::run_command("nmcli", true, ["networking", "connectivity", "check"]);
    assert_eq!(reg_cmd.output, "full");
    assert_eq!(priv_cmd.output, "full");
}

// process spawner test
// requires Firefox dev edition and okular to be installed
#[test]
fn spawn_proc_test() {
    utils::spawn_process("firefox-developer-edition", false, false, ["aercloud-systems.net"]);
    utils::spawn_process("okular", false, true, ["README.md"]);
    utils::spawn_process("echo", false, false, ["yes"]);
}

#[test]
fn user_input() {
    let name = get_input("What is your name?");
    println!("Your name is {name}.");
}

#[cfg(feature = "futures")]
#[test]
fn futures() {
    let val = exec_future(future_fn());
    println!("Value from async function: {val}");
}

#[cfg(feature = "futures")]
async fn future_fn() -> String {
    let log = Log::new("test", "coof-testing");
    log.line_basic("async rust function being executed!", true);
    log.line_basic(format!("2 + 2 is {}, not 5!", 2 + 2), true);
    "yes".to_string()
}

#[test]
fn map_macro() {
    let map = hash_map! {
        "yes" => 0.,
        "no" => 1.,
        "pi" => std::f64::consts::PI
    };

    for (key, val) in map.iter() {
        println!("Key: {key}, Value: {val}");
    }
}

#[test]
fn chainable_macro() {
    // this just needs to compile w/o error
    #[allow(unused)]
    trait TestTrait: Sized {
        #[chainable]
        fn set_something(&mut self, val: u32);

        #[chainable]
        fn set_something_else(&mut self, val: u32) {
            self.set_something(val);
        }
    }

    struct Test1 {
        field_0: bool
    }

    impl Test1 {
        fn new() -> Self {
            Test1 {
                field_0: false
            }
        }

        #[chainable]
        fn set_field_0(&mut self, val: bool) {
            self.field_0 = val;
        }
    }

    #[chainable]
    #[derive(Default, Debug)]
    struct Test2 {
        #[chainable(doc = "Documentation for `field_0`")]
        field_0: bool,
        field_1: usize,

        #[chainable(use_into_impl)]
        field_2: f64,

        #[chainable(collapse_option, doc = "Documentation for `opt_field`")]
        opt_field: Option<usize>
    }

    // this just needs to compile w/o error
    #[allow(unused)]
    #[derive(Default)]
    struct Test3 {
        opt_field: Option<u32>
    }

    impl Test3 {
        #[chainable(collapse_options, use_into_impl)]
        fn set_opt_field(&mut self, opt_field: Option<u32>, other: f64) {
            println!("{other}");
            self.opt_field = opt_field;
        }
    }

    let test1 = Test1::new().field_0(true);
    println!("Value of field0: {}", test1.field_0);

    let test2 = Test2::default()
        .field_0(false)
        .field_1(100)
        .field_2(std::f64::consts::PI)
        .opt_field(1);

    println!("{test2:?}");
}

#[test]
fn new_macro() {
    // this just needs to compile w/o error
    #[allow(unused)]
    #[derive(Default, New)]
    struct Test {
        a: usize,
        b: f64,
        c: f32
    }

    // this just needs to compile w/o error
    #[allow(unused)]
    #[derive(Default, New)]
    struct Test2<A, B, C> {
        a: A,
        b: B,
        c: C
    }
}

#[test]
fn encapsulated_macro() {
    // this just needs to compile w/o error
    #[allow(unused)]
    #[derive(Default, New)]
    #[encapsulated]
    struct Test {
        #[getter(copy_val)]
        #[setter]
        a: usize,

        #[getter]
        #[setter(use_into_impl)]
        b: f64,
        c: f32,

        #[setter]
        #[chainable]
        chainable: u32,

        #[setter]
        #[chainable(collapse_option)]
        thing1: Option<u32>,

        #[setter]
        #[chainable(collapse_option, use_into_impl)]
        thing2: Option<usize>
    }
}