mock_gen 0.1.22

Interface and mock generator for C++ classes
//! generate mocks functions
use crate::prelude::*;
use regex::Regex;
use std::fs;
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::string;

static DOCTEST_INCLUDE: &str = "#include \"doctest.h\"";
static TROMPELOEIL_INCLUDE: &str = "#include \"trompeloeil.hpp\"";
static FORMAT_OFF: &str = "// clang-format off";
static FORMAT_ON: &str = "// clang-format on";
static GEN_STRING_START: &str = "/* generated mock code */";
static GEN_STRING_END: &str = "/* generated mock code end */";

pub fn generate_mock_string(function: &str) -> Result<String> {
    //regex to get arguments
    let re = Regex::new(r"\(([^()]*)\)").unwrap();
    let mut arguments = re.find(&function).unwrap().as_str();
    let mut args: Vec<&str> = arguments.split([',']).collect();
    let parts: Vec<&str> = function.split([' ', '(']).collect();
    let mut return_param: String = parts[0].to_owned();

    let mut func_name: String = String::new();
    func_name = parts[1].trim().to_owned();
    if func_name.contains("*") {
        func_name = func_name.replace("*", "");
        return_param = return_param + "*";
    } else if func_name.contains("&") {
        func_name = func_name.replace("&", "");
        return_param = return_param + "&";
    }

    let mut arg_count = 0;
    let mut arg_vec: Vec<String> = Vec::new();
    let mut edited_string: String = String::new();
    for typename in args {
        if typename.contains("void") || typename.contains("()") {
            break;
        }
        arg_count += 1;
        let mut trimmed_string = typename;
        if let Some((m, _)) = typename.split_once(" )") {
            trimmed_string = m;
        }
        let new_string = trimmed_string;
        let mut arg: Vec<&str> = new_string.split(" ").collect();
        if arg.len() == 1 {
            println!("missing parameter name for {}", func_name);
        }
        let typename = arg.pop().unwrap_or("");
        let arg_string = arg.join(" ");
        let mut arg_string = arg_string.to_owned();
        edited_string = arg_string.replace('(', "");
        edited_string = edited_string.replace(')', "");
        if typename.contains("**") {
            edited_string = edited_string + "**";
        } else if typename.contains("*") {
            edited_string = edited_string + "*";
        }
        if typename.contains("&") {
            edited_string = edited_string + "&";
        }
        arg_vec.push(edited_string);
    }

    let mut conststring = "";

    let args_string = arg_vec.join(", ");
    if function.contains(") const") {
        conststring = "_CONST";
    }
    let mock_string = format!(
        "\tMAKE{}_MOCK{}({}, {}({}), override);",
        conststring, arg_count, func_name, return_param, args_string
    );
    Ok(mock_string)
}

pub fn generate_mock_file(
    output_path: &str,
    file_name: &str,
    classname: &str,
    inheritance: &str,
    funcs: &Vec<String>,
) -> Result<()> {
    let mut output_path_owned = output_path.to_owned();
    if !output_path.contains("./") {
        let string_name = &output_path.to_owned();
        output_path_owned = "./".to_owned() + string_name;
    }
    if !fs::metadata(&output_path_owned).is_ok() {
        fs::create_dir(&output_path_owned)?;
    }
    let mut output_lines: Vec<String> = Vec::new();
    let mut file_exists = false;
    let (_, filename) = file_name.rsplit_once(['\\', '/']).unwrap();
    let file_path = output_path.to_owned() + "\\mock_" + filename;
    let mut lines: Vec<String> = Vec::new();
    let mut index = 0;
    if (Path::new(&file_path).exists()) {
        file_exists = true;
        lines = BufReader::new(
            OpenOptions::new()
                .read(true)
                .open(file_path.as_str())
                .unwrap(),
        )
        .lines()
        .map(|line| line.unwrap())
        .collect::<Vec<String>>();

        index = lines.iter().position(|r| r == GEN_STRING_START).unwrap();
        loop {
            if (lines[index] == GEN_STRING_END) {
                lines.remove(index);
                break;
            }
            lines.remove(index);
        }
    };

    for (idx, line) in lines.iter().enumerate() {
        if (idx >= index) {
            output_lines.insert(index, GEN_STRING_START.to_owned());
            break;
        }
        output_lines.push(line.to_string());
    }

    for func in funcs.iter() {
        output_lines.push(generate_mock_string(func)?);
    }

    let class_name_string = "class mock_".to_owned() + classname;
    let include_string = format!("#include \"{}\"", filename);
    //insert class stuff
    output_lines.insert(index, "{".to_owned());
    output_lines.insert(index, class_name_string.to_owned() + inheritance);
    output_lines.insert(index, "".to_owned());
    output_lines.insert(index, "".to_owned());
    output_lines.insert(index, include_string.to_owned());
    output_lines.insert(index, TROMPELOEIL_INCLUDE.to_owned());
    output_lines.insert(index, DOCTEST_INCLUDE.to_owned());
    output_lines.insert(index, "".to_owned());
    output_lines.insert(index, "".to_owned());

    output_lines.insert(index, GEN_STRING_START.to_owned());

    if (!file_exists) {
        output_lines.insert(
            0,
            "#ifndef _MOCK_".to_owned() + &filename.to_ascii_uppercase().replace(".", "_"),
        );
        output_lines.insert(
            1,
            "#define _MOCK_".to_owned() + &filename.to_ascii_uppercase().replace(".", "_"),
        );
    }

    output_lines.push("};".to_owned());
    output_lines.push(GEN_STRING_END.to_owned());

    if (!file_exists) {
        output_lines.push("#endif".to_owned());
    } else {
        for line in lines[index..].iter() {
            output_lines.push(line.to_string());
        }
    }

    if Path::new(&file_path).exists() {
        fs::remove_file(&file_path).unwrap();
    }

    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(file_path)
        .unwrap();

    for line in output_lines {
        writeln!(file, "{}", line).unwrap();
    }
    Ok(())
}

pub fn generate_c_wrapper(output_path: &str, file_name: &str, funcs: &Vec<String>) -> Result<()> {
    let mut output_path_owned = output_path.to_owned();
    if !output_path.contains("./") {
        let string_name = &output_path.to_owned();
        output_path_owned = "./".to_owned() + string_name;
    }
    if !fs::metadata(&output_path_owned).is_ok() {
        fs::create_dir(&output_path_owned)?;
    }

    let (_, filename) = file_name.rsplit_once(['\\', '/']).unwrap();
    let mock_path = "mock_".to_owned() + filename;
    let file_path = output_path_owned + "\\" + filename;
    let mock_name = "mock_".to_owned() + filename.trim_end_matches(".h") + "_c";
    let mut lines: Vec<String> = Vec::new();
    if (Path::new(&file_path).exists()) {
        lines = BufReader::new(
            OpenOptions::new()
                .read(true)
                .open(file_path.as_str())
                .unwrap(),
        )
        .lines()
        .map(|line| line.unwrap())
        .collect::<Vec<String>>();
        let index = lines.iter().position(|r| r == GEN_STRING_START).unwrap();
        loop {
            lines.remove(index);
            if (lines[index] == GEN_STRING_END) {
                lines.remove(index);
                break;
            }
        }
    };

    if Path::new(&file_path).exists() {
        fs::remove_file(&file_path).unwrap();
    }

    let mut output_lines: Vec<String> = Vec::new();

    let mut index = lines.len();

    for line in lines {
        output_lines.push(line.to_string());
    }

    let include_string = format!("#include \"{}\"", mock_path);
    output_lines.push(GEN_STRING_START.to_owned());
    output_lines.push(include_string);
    output_lines.push("".to_owned());
    output_lines.push(mock_name + " api_mock;");
    output_lines.push("".to_owned());
    output_lines.push("extern \"C\"{".to_owned());

    for func in funcs.iter() {
        let func_name = func.trim_end_matches(";");
        output_lines.push("".to_owned());
        output_lines.push("\t".to_owned() + func_name);
        output_lines.push("\t{".to_owned());
        let func_name: Vec<&str> = func.split(" ").collect();
        let line = func_name.join(" ");
        //need to remove types
        let regex = Regex::new(r"\(([^()]*)\)").unwrap();
        let mut args: Vec<String> = Vec::new();
        for params in regex.captures_iter(&line) {
            let mut params = params.get(0).unwrap().as_str().to_string();
            params = params.replace(")", "");
            params = params.replace("(", "");
            for split in params.split(',') {
                let mut trimmed_string = split.trim();
                if let Some(argument_split) = trimmed_string.rsplit_once(' ') {
                    trimmed_string = argument_split.1;
                } else {
                    continue;
                }
                let name = trimmed_string;
                let mut name_string = name.to_owned();
                name_string = name_string.replace("*", "");
                name_string = name_string.replace("&", "");
                args.push(name_string.to_owned())
            }
        }
        let mut func_name: Vec<&str> = func.split("(").collect();

        let arg_string = args.join(", ");
        let args_call = "( ".to_owned() + arg_string.as_str() + " );";

        let func_call = func_name[0];
        let func_name: Vec<&str> = func_call.split(" ").collect();
        let name = func_name[1];

        let module_call = "\t\treturn api_mock.".to_owned() + name + args_call.as_str();
        output_lines.push(module_call);
        output_lines.push("\t}".to_owned());
        output_lines.push("".to_owned());
    }

    output_lines.push("}".to_owned());
    output_lines.push(GEN_STRING_END.to_owned());

    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(file_path)
        .unwrap();

    for line in output_lines {
        writeln!(file, "{}", line).unwrap();
    }
    Ok(())
}

pub fn generate_cmock_file(output_path: &str, path: &str, funcs: &Vec<String>) -> Result<()> {
    //create fake mock class name
    let (_, filename) = path.rsplit_once(['\\', '/']).unwrap();
    let mock_name = filename.trim_end_matches(".h").to_owned() + "_c";
    generate_mock_file(output_path, path, &mock_name, "", funcs);
    generate_c_wrapper(output_path, path, funcs);
    Ok(())
}