use clap::Parser;
use nanalogue_core::{Error, SimulationConfig, simulate_mod_bam};
#[derive(Parser, Debug)]
#[command(author, version,
about = "Simulate BAM with or without modifications. Aimed at developers who wish to test their BAM parsers",
long_about = None)]
struct Cli {
json: String,
bam: String,
fasta: String,
}
fn main() {
let cli = Cli::parse();
match run(&cli) {
Ok(()) => {}
Err(e) => {
eprintln!("Error during execution: {e}");
std::process::exit(1);
}
}
}
fn run(cli: &Cli) -> Result<(), Error> {
let json_str = std::fs::read_to_string(&cli.json)?;
let config: SimulationConfig = serde_json::from_str(&json_str)?;
simulate_mod_bam::run(config, &cli.bam, &cli.fasta)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use uuid::Uuid;
#[test]
fn run_creates_output_files() {
let temp_path = env::temp_dir().join(format!("nanalogue_test_{}", Uuid::new_v4()));
fs::create_dir_all(&temp_path).expect("failed to create temp dir");
let json_config = r#"{
"contigs": {
"number": 2,
"len_range": [100, 200],
"repeated_seq": "ACGTACGT"
},
"reads": [
{
"number": 10,
"mapq_range": [10, 30],
"base_qual_range": [20, 40],
"len_range": [0.1, 0.9],
"barcode": "ACGTAA",
"mods": [{
"base": "C",
"is_strand_plus": true,
"mod_code": "m",
"win": [5, 3],
"mod_range": [[0.3, 0.7], [0.1, 0.5]]
}]
}
]
}"#;
let json_path = temp_path.join("config.json");
fs::write(&json_path, json_config).expect("failed to write JSON config");
let bam_path = temp_path.join("output.bam");
let fasta_path = temp_path.join("output.fasta");
let cli = Cli {
json: json_path.to_string_lossy().to_string(),
bam: bam_path.to_string_lossy().to_string(),
fasta: fasta_path.to_string_lossy().to_string(),
};
run(&cli).expect("run should succeed with valid config");
assert!(bam_path.exists(), "BAM file should be created");
assert!(fasta_path.exists(), "FASTA file should be created");
let bai_path = temp_path.join("output.bam.bai");
assert!(bai_path.exists(), "BAI index file should be created");
let bam_meta = fs::metadata(&bam_path).expect("failed to get BAM metadata");
let fasta_meta = fs::metadata(&fasta_path).expect("failed to get FASTA metadata");
let index_meta = fs::metadata(&bai_path).expect("failed to get BAI metadata");
assert!(bam_meta.len() > 0, "BAM file should not be empty");
assert!(fasta_meta.len() > 0, "FASTA file should not be empty");
assert!(index_meta.len() > 0, "BAI index file should not be empty");
fs::remove_dir_all(&temp_path).expect("failed to clean up temp dir");
}
#[test]
fn run_fails_with_invalid_json() {
let temp_path = env::temp_dir().join(format!("nanalogue_test_{}", Uuid::new_v4()));
fs::create_dir_all(&temp_path).expect("failed to create temp dir");
let json_path = temp_path.join("invalid.json");
fs::write(&json_path, "{ invalid json }").expect("failed to write invalid JSON");
let bam_path = temp_path.join("output.bam");
let fasta_path = temp_path.join("output.fasta");
let cli = Cli {
json: json_path.to_string_lossy().to_string(),
bam: bam_path.to_string_lossy().to_string(),
fasta: fasta_path.to_string_lossy().to_string(),
};
let result = run(&cli);
assert!(result.is_err(), "run should fail with invalid JSON");
fs::remove_dir_all(&temp_path).expect("failed to clean up temp dir");
}
#[test]
fn run_fails_with_missing_json_file() {
let temp_path = env::temp_dir().join(format!("nanalogue_test_{}", Uuid::new_v4()));
fs::create_dir_all(&temp_path).expect("failed to create temp dir");
let cli = Cli {
json: temp_path
.join("nonexistent.json")
.to_string_lossy()
.to_string(),
bam: temp_path.join("output.bam").to_string_lossy().to_string(),
fasta: temp_path.join("output.fasta").to_string_lossy().to_string(),
};
let result = run(&cli);
assert!(
result.is_err(),
"run should fail when JSON file doesn't exist"
);
fs::remove_dir_all(&temp_path).expect("failed to clean up temp dir");
}
}