thag_rs 0.1.6

A versatile cross-platform script runner and REPL for Rust snippets, expressions and programs. Accepts a script file or dynamic options.
#[cfg(test)]
use cargo_toml::Manifest;
use thag_rs::builder::{build, generate, run};
use thag_rs::cmd_args::Cli;
use thag_rs::{code_utils, escape_path_for_windows, execute, TMPDIR};
use thag_rs::{BuildState, ProcFlags};
// use sequential_test::sequential;
use std::env::current_dir;
use std::fs::{self, OpenOptions};
use std::io::Write;

// Set environment variables before running tests
fn set_up() {
    std::env::set_var("TEST_ENV", "1");
    std::env::set_var("VISUAL", "cat");
    std::env::set_var("EDITOR", "cat");
}

// Helper function to create a sample Cli structure
fn create_sample_cli(script: Option<String>) -> Cli {
    Cli {
        script,
        args: Vec::new(),
        expression: None,
        ..Default::default()
    }
}

// Helper function to create a sample BuildState structure.
// Requires the sample script to be in tests/assets.
fn create_sample_build_state(source_name: &str) -> BuildState {
    set_up();
    let source_stem: &str = source_name
        .strip_suffix(thag_rs::RS_SUFFIX)
        .expect("Problem stripping Rust suffix");
    let current_dir = current_dir().expect("Could not get current dir");
    let working_dir_path = current_dir.clone();
    let cargo_home = home::cargo_home().expect("Could not get Cargo home");
    let target_dir_path = TMPDIR.join("thag_rs").join(source_stem);
    fs::create_dir_all(target_dir_path.clone()).expect("Failed to create script directory");
    let target_path = target_dir_path
        .clone()
        .join("target/debug")
        .join(source_stem);
    let cargo_toml_path = target_dir_path.clone().join("Cargo.toml");
    let source_dir_path = current_dir.clone().join("tests/assets");
    let source_path = current_dir.clone().join("tests/assets").join(source_name);
    BuildState {
        working_dir_path,
        source_stem: source_stem.into(),
        source_name: source_name.into(),
        source_dir_path,
        source_path,
        cargo_home,
        target_dir_path,
        target_path,
        cargo_toml_path,
        rs_manifest: None,
        cargo_manifest: None,
        must_gen: true,
        must_build: true,
        build_from_orig_source: false,
    }
}

#[test]
// #[sequential]
fn test_builder_execute_dynamic_script() {
    set_up();
    let mut args = create_sample_cli(Some(
        "tests/assets/determine_if_known_type_trait_t.rs".to_string(),
    ));
    args.force = true;
    let result = execute(&mut args);
    assert!(result.is_ok());
}

// Any test of the REPL is problematic because reedline will panic
// with a message that the current cursor position can't be found.
// #[test]
// fn test_builder_execute_repl_script() {
// let mut args = create_sample_cli(None);
// args.repl = true;
//     let result = execute(args);
//     assert!(result.is_ok());
// }

#[test]
fn test_builder_generate_source_file() {
    set_up();
    let script_name = "fib_fac_lite_t.rs";
    let mut build_state = create_sample_build_state(script_name);
    build_state.must_gen = true;
    build_state.must_build = true;
    build_state.cargo_toml_path = build_state.target_dir_path.clone().join("Cargo.toml");
    let cargo_toml = format!(
        r#"[package]
        name = "fib_fac_lite_t"
        version = "0.0.1"
        edition = "2021"

        [dependencies]
        itertools = "0.13.0"

        [features]

        [patch]

        [workspace]

        [[bin]]
        path = "{}/thag_rs/fib_fac_lite_t/fib_fac_lite_t.rs"
        name = "fib_fac_lite_t"
"#,
        escape_path_for_windows(TMPDIR.display().to_string().as_str())
    );
    let cargo_manifest = Manifest::from_str(&cargo_toml).expect("Could not parse manifest string");
    build_state.cargo_manifest = Some(cargo_manifest);

    let rs_source = code_utils::read_file_contents(&build_state.source_path)
        .expect("Error reading script contents");
    let proc_flags = ProcFlags::empty();
    let result = generate(&build_state, Some(&rs_source), &proc_flags);
    assert!(result.is_ok());
    assert!(build_state.target_dir_path.join(script_name).exists());
    assert!(build_state.cargo_toml_path.exists());
}

#[test]
// #[sequential]
fn test_builder_build_cargo_project() {
    set_up();
    let source_name = "bitflags_t.rs";
    let source_stem: &str = source_name
        .strip_suffix(thag_rs::RS_SUFFIX)
        .expect("Problem stripping Rust suffix");

    let current_dir = current_dir().expect("Could not get current dir");
    let source_path = current_dir.join("tests/assets").join(source_name);
    let cargo_home = home::cargo_home().expect("Could not get Cargo home");
    let target_dir_path = TMPDIR.join("thag_rs").join(source_stem);
    fs::create_dir_all(target_dir_path.clone()).expect("Failed to create script directory");
    let cargo_toml_path = target_dir_path.clone().join("Cargo.toml");
    let cargo_toml = format!(
        r#"[package]
name = "bitflags_t"
version = "0.0.1"
edition = "2021"

[dependencies]
bitflags = "2.5.0"

[features]

[patch]

[workspace]

[[bin]]
path = "{}/thag_rs/bitflags_t/bitflags_t.rs"
name = "bitflags_t"
"#,
        escape_path_for_windows(TMPDIR.display().to_string().as_str())
    );

    std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(cargo_toml_path.clone())
        .expect("Error creating Cargo.toml");

    let mut cargo_toml_file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(cargo_toml_path.clone())
        .expect("Error opening Cargo.toml");

    cargo_toml_file
        .write_all(cargo_toml.as_bytes())
        .expect("error writing Cargo.toml");

    let target_rs_path = target_dir_path.clone().join(source_name);

    let rs_source =
        code_utils::read_file_contents(&source_path).expect("Error reading script contents");
    let _source_file = code_utils::write_source(&target_rs_path, &rs_source)
        .expect("Problem writing source to target path");
    // println!("source_file={source_file:#?}");

    let build_state = BuildState {
        working_dir_path: current_dir.clone(),
        source_stem: source_stem.into(),
        source_name: source_name.into(),
        source_dir_path: current_dir.join("tests/assets"),
        source_path,
        cargo_home,
        target_path: target_dir_path
            .clone()
            .join("target/debug")
            .join(source_stem),
        cargo_toml_path,
        target_dir_path,
        rs_manifest: None,
        cargo_manifest: None,
        must_gen: true,
        must_build: true,
        build_from_orig_source: false,
    };
    dbg!(&build_state);
    let proc_flags = ProcFlags::empty();
    let result = build(&proc_flags, &build_state);
    assert!(result.is_ok());
}

#[test]
// #[sequential]
fn test_builder_run_script() {
    set_up();
    let source_name = "fib_fac_dashu_t.rs";
    let source_stem: &str = source_name
        .strip_suffix(thag_rs::RS_SUFFIX)
        .expect("Problem stripping Rust suffix");
    let target_dir_path = TMPDIR
        .join("thag_rs")
        .join(source_stem)
        .join("target/debug");
    let target_path = if cfg!(windows) {
        target_dir_path.join(source_stem.to_string() + ".exe")
    } else {
        target_dir_path.join(source_stem)
    };

    // Remove executable if it exists, and check
    let result = fs::remove_file(&target_path);
    eprintln!("Result of fs::remove_file({target_path:?}): {result:?}");
    assert!(!target_path.exists());

    // Generate and build executable, and check it exists.
    let mut args = create_sample_cli(Some("tests/assets/fib_fac_dashu_t.rs".to_string()));
    args.generate = true;
    args.build = true;
    let result = execute(&mut args);
    assert!(result.is_ok());
    println!("target_path={target_path:#?}");
    assert!(target_path.exists());

    // Finally, run it
    let cli = create_sample_cli(Some(format!("tests/assets/{source_name}")));
    let build_state = create_sample_build_state(source_name);
    dbg!(&build_state);
    let proc_flags = ProcFlags::empty();
    let result = run(&proc_flags, &cli.args, &build_state);
    assert!(result.is_ok());
}