use primer3::{
SolutionConditions, ThermoArgs, TmParams, calc_end_stability, calc_end_stability_with,
calc_hairpin, calc_hairpin_with, calc_heterodimer, calc_heterodimer_with, calc_homodimer,
calc_homodimer_with, calc_tm, calc_tm_with, reverse_complement,
};
#[test]
fn test_calc_tm_m13_forward() {
let tm = calc_tm("GTAAAACGACGGCCAGT").unwrap();
assert!((48.0..=58.0).contains(&tm), "M13 forward Tm = {tm:.2}, expected roughly 53");
}
#[test]
fn test_calc_tm_poly_a() {
let tm = calc_tm("AAAAAAAAAAAAAAAAAAA").unwrap();
assert!(tm < 40.0, "poly-A Tm = {tm:.2}, expected < 40");
}
#[test]
fn test_calc_tm_gc_rich() {
let tm = calc_tm("GCGCGCGCGCGCGCGCGCGC").unwrap();
assert!(tm > 65.0, "GC-rich Tm = {tm:.2}, expected > 65");
}
#[test]
fn test_calc_tm_empty_seq() {
let result = calc_tm("");
assert!(result.is_err());
}
#[test]
fn test_calc_tm_custom_conditions() {
let default_tm = calc_tm("GTAAAACGACGGCCAGT").unwrap();
let high_salt_params = TmParams {
conditions: SolutionConditions::default().with_mv_conc(200.0),
..Default::default()
};
let high_salt_tm = calc_tm_with("GTAAAACGACGGCCAGT", &high_salt_params).unwrap();
assert!(
high_salt_tm > default_tm,
"Higher salt Tm ({high_salt_tm:.2}) should be > default ({default_tm:.2})"
);
}
#[test]
fn test_calc_hairpin_palindrome() {
let result = calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
assert!(
result.tm() > 0.0 || result.dg() < 0.0,
"Hairpin result: tm={:.2}, dg={:.0}",
result.tm(),
result.dg(),
);
}
#[test]
fn test_calc_hairpin_no_structure() {
let result = calc_hairpin("ATCGATCG").unwrap();
let _ = result.tm();
let _ = result.dg();
}
#[test]
fn test_calc_hairpin_empty() {
assert!(calc_hairpin("").is_err());
}
#[test]
fn test_calc_hairpin_too_long() {
let long_seq = "A".repeat(61);
assert!(calc_hairpin(&long_seq).is_err());
}
#[test]
fn test_calc_hairpin_with_structure() {
let args = ThermoArgs { output_structure: true, ..Default::default() };
let result = calc_hairpin_with("CCCCCATCCGATCAGGGGG", &args).unwrap();
if result.structure_found() {
assert!(
result.ascii_structure().is_some(),
"Expected ASCII structure when structure_found=true and output_structure=true"
);
}
}
#[test]
fn test_calc_homodimer() {
let result = calc_homodimer("AGTCTAGTCTATCGATCG").unwrap();
let _ = result.tm();
let _ = result.dg();
let _ = result.dh();
let _ = result.ds();
}
#[test]
fn test_calc_homodimer_empty() {
assert!(calc_homodimer("").is_err());
}
#[test]
fn test_calc_homodimer_custom_conditions() {
let args = ThermoArgs {
conditions: SolutionConditions::default().with_mv_conc(100.0),
..Default::default()
};
let result = calc_homodimer_with("AGTCTAGTCTATCGATCG", &args).unwrap();
let _ = result.tm();
}
#[test]
fn test_calc_heterodimer_complementary() {
let result = calc_heterodimer("AAAAAAAAAA", "TTTTTTTTTT").unwrap();
assert!(result.dg() < 0.0, "Complementary heterodimer dG = {:.0}, expected < 0", result.dg());
}
#[test]
fn test_calc_heterodimer_non_complementary() {
let result = calc_heterodimer("AAAAAAAAAA", "AAAAAAAAAA").unwrap();
let _ = result.dg();
}
#[test]
fn test_calc_heterodimer_empty() {
assert!(calc_heterodimer("", "ATCG").is_err());
assert!(calc_heterodimer("ATCG", "").is_err());
}
#[test]
fn test_calc_heterodimer_both_too_long() {
let long1 = "A".repeat(61);
let long2 = "T".repeat(61);
assert!(calc_heterodimer(&long1, &long2).is_err());
}
#[test]
fn test_calc_heterodimer_one_long_ok() {
let short = "ATCGATCGATCG";
let long = "T".repeat(100);
let result = calc_heterodimer(short, &long);
assert!(result.is_ok(), "One short + one long should work");
}
#[test]
fn test_calc_heterodimer_with_structure() {
let args = ThermoArgs { output_structure: true, ..Default::default() };
let result = calc_heterodimer_with("AAAAAAAAAA", "TTTTTTTTTT", &args).unwrap();
if result.structure_found() {
assert!(result.ascii_structure().is_some());
}
}
#[test]
fn test_calc_end_stability() {
let result = calc_end_stability("AGCTATTTTTTTTTTT", "CATGATTTTTTTTTTTTTT").unwrap();
let _ = result.tm();
let _ = result.dg();
}
#[test]
fn test_calc_end_stability_custom() {
let args = ThermoArgs {
conditions: SolutionConditions::default().with_mv_conc(100.0),
..Default::default()
};
let result = calc_end_stability_with("AGCTATTTTTTTTTTT", "CATGATTTTTTTTTTTTTT", &args).unwrap();
let _ = result.tm();
}
#[test]
fn test_thermo_result_display() {
let result = calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
let display = format!("{result}");
assert!(display.contains("ThermoResult"));
assert!(display.contains("tm="));
}
#[test]
fn test_thermo_result_approx_eq() {
let r1 = calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
let r2 = calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
assert!(r1.approx_eq(&r2, 0.01), "Same input should give same result");
}
#[test]
fn test_reverse_complement() {
assert_eq!(reverse_complement("ATCG"), "CGAT");
assert_eq!(reverse_complement(""), "");
assert_eq!(reverse_complement("A"), "T");
assert_eq!(reverse_complement("AAAA"), "TTTT");
assert_eq!(reverse_complement("aAtTgGcC"), "GgCcAaTt");
}
#[test]
fn test_design_primers_basic() {
use primer3::{PrimerSettings, SequenceArgs, design_primers};
let template = "GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGA\
ATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCA\
CAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGC\
CTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTT\
CCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGC\
GGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGA\
CTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAA\
GGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATG\
TGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGG\
CGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTC";
let seq_args = SequenceArgs::builder().sequence(template).target(200, 50).build().unwrap();
let settings = PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(5)
.build()
.unwrap();
let result = design_primers(&seq_args, &settings, None, None).unwrap();
assert!(
result.num_pairs() > 0,
"Expected at least one primer pair, got {}. Left stats: {:?}",
result.num_pairs(),
result.left_stats(),
);
let pair = &result.pairs()[0];
assert!(!pair.left().sequence().is_empty());
assert!(!pair.right().sequence().is_empty());
assert!(pair.left().tm() > 50.0 && pair.left().tm() < 70.0);
assert!(pair.right().tm() > 50.0 && pair.right().tm() < 70.0);
assert!(pair.product_size() > 0);
assert!(pair.product_size() <= 300);
}
#[test]
fn test_design_primers_no_target() {
use primer3::{PrimerSettings, SequenceArgs, design_primers};
let seq_args = SequenceArgs::builder().sequence("ATCGATCG").build().unwrap();
let settings = PrimerSettings::default();
let result = design_primers(&seq_args, &settings, None, None);
if let Ok(r) = result {
assert_eq!(r.num_pairs(), 0, "No pairs expected for tiny sequence");
}
}
#[test]
fn test_design_primers_check_mode() {
use primer3::{PrimerSettings, PrimerTask, SequenceArgs, design_primers};
let template = "GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGA\
ATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCA\
CAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGC\
CTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTT\
CCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGC";
let seq_args = SequenceArgs::builder()
.sequence(template)
.left_primer("GCTTGCATGCCTGCAGGTCG")
.right_primer("GCGCGTTGGCCGATTCATTA")
.build()
.unwrap();
let settings = PrimerSettings::builder()
.task(PrimerTask::CheckPrimers)
.primer_opt_tm(60.0)
.primer_min_tm(50.0)
.primer_max_tm(70.0)
.product_size_range(100, 300)
.build()
.unwrap();
let result = design_primers(&seq_args, &settings, None, None).unwrap();
assert!(
result.num_pairs() <= 1,
"Check mode should return at most 1 pair, got {}",
result.num_pairs()
);
}
#[test]
fn test_design_primers_with_misprime_lib() {
use primer3::{PrimerSettings, SequenceArgs, SequenceLibrary, design_primers};
let template = "GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGA\
ATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCA\
CAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGC\
CTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTT\
CCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGC\
GGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGA\
CTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAA\
GGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATG\
TGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGG\
CGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTC";
let seq_args = SequenceArgs::builder().sequence(template).target(200, 50).build().unwrap();
let settings = PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(5)
.build()
.unwrap();
let mut lib = SequenceLibrary::new();
lib.add("SIMPLE_REPEAT", "ATATATATATATATATATATATAT");
lib.add_weighted("ALU_FRAG", "GGCCGGGCGCGGTGGCTCACGCCTGTAAT", 2.0);
let result = design_primers(&seq_args, &settings, Some(&lib), None).unwrap();
assert!(
result.num_pairs() > 0,
"Expected primer pairs with mispriming library, got {}",
result.num_pairs()
);
}
#[test]
fn test_design_primers_with_empty_lib() {
use primer3::{PrimerSettings, SequenceArgs, SequenceLibrary, design_primers};
let template = "GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGA\
ATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCA\
CAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGC\
CTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTT\
CCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGC";
let seq_args = SequenceArgs::builder().sequence(template).target(100, 50).build().unwrap();
let settings = PrimerSettings::builder().product_size_range(75, 300).build().unwrap();
let lib = SequenceLibrary::new();
let result = design_primers(&seq_args, &settings, Some(&lib), None);
assert!(result.is_ok());
}
#[test]
fn test_thread_safety_tm() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
for _ in 0..10 {
let tm = calc_tm("GTAAAACGACGGCCAGT").unwrap();
assert!(tm > 40.0 && tm < 70.0);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn test_thread_safety_hairpin() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
for _ in 0..10 {
let result = calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
let _ = result.tm();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}