macro_rules! assert_approx {
($a:expr, $b:expr, $tol:expr, $($msg:tt)*) => {
assert!(
($a - $b).abs() <= $tol,
"{}: expected {}, got {} (diff={})",
format!($($msg)*), $b, $a, ($a - $b).abs(),
);
};
}
const TM_TOL: f64 = 0.001;
#[test]
fn tm_all_method_salt_combinations() {
use primer3::*;
let seq = "GTAAAACGACGGCCAGT";
let combos: &[(TmMethod, SaltCorrectionMethod)] = &[
(TmMethod::Breslauer, SaltCorrectionMethod::Schildkraut),
(TmMethod::Breslauer, SaltCorrectionMethod::SantaLucia),
(TmMethod::Breslauer, SaltCorrectionMethod::Owczarzy),
(TmMethod::SantaLucia, SaltCorrectionMethod::Schildkraut),
(TmMethod::SantaLucia, SaltCorrectionMethod::SantaLucia),
(TmMethod::SantaLucia, SaltCorrectionMethod::Owczarzy),
(TmMethod::SantaLucia2004, SaltCorrectionMethod::Schildkraut),
(TmMethod::SantaLucia2004, SaltCorrectionMethod::SantaLucia),
(TmMethod::SantaLucia2004, SaltCorrectionMethod::Owczarzy),
];
let mut tms = Vec::new();
for (tm_method, salt_method) in combos {
let params = TmParams {
tm_method: *tm_method,
salt_correction_method: *salt_method,
..Default::default()
};
let tm = calc_tm_with(seq, ¶ms).unwrap();
assert!(
tm > 30.0 && tm < 100.0,
"{tm_method:?}/{salt_method:?}: Tm {tm} out of reasonable range"
);
tms.push(tm);
}
tms.sort_by(|a, b| a.partial_cmp(b).unwrap());
tms.dedup_by(|a, b| (*a - *b).abs() < 0.01);
assert!(tms.len() >= 4, "expected at least 4 distinct Tm values, got {}", tms.len());
}
#[test]
fn tm_sequence_lengths() {
let tm5 = primer3::calc_tm("ATCGA").unwrap();
assert!(tm5 > -50.0 && tm5 < 40.0, "5mer Tm: {tm5}");
let tm10 = primer3::calc_tm("ATCGATCGAT").unwrap();
assert!(tm10 > tm5, "10mer should be higher than 5mer");
let tm20 = primer3::calc_tm("ATCGATCGATCGATCGATCG").unwrap();
assert!(tm20 > tm10, "20mer should be higher than 10mer");
let seq60 = "ATCGATCG".repeat(7) + "ATCG";
assert_eq!(seq60.len(), 60);
let tm60 = primer3::calc_tm(&seq60).unwrap();
assert!(tm60 > 50.0, "60mer Tm should be reasonable: {tm60}");
let seq61 = "ATCGATCG".repeat(7) + "ATCGA";
assert_eq!(seq61.len(), 61);
let tm61 = primer3::calc_tm(&seq61).unwrap();
assert!(tm61 > 50.0, "61mer Tm should be reasonable: {tm61}");
}
#[test]
fn tm_extreme_gc_content() {
let tm_gc = primer3::calc_tm("GCGCGCGCGCGCGCGCGCGC").unwrap();
assert!(tm_gc > 70.0, "all-GC 20mer Tm should be high: {tm_gc}");
let tm_at = primer3::calc_tm("ATAATATATAATATATATA").unwrap();
assert!(tm_at < 45.0, "all-AT 18mer Tm should be low: {tm_at}");
assert!(tm_gc - tm_at > 30.0, "GC vs AT Tm difference should be large");
}
#[test]
fn tm_dmso_lowers_tm() {
let seq = "GTAAAACGACGGCCAGT";
let base_tm = primer3::calc_tm(seq).unwrap();
let params = primer3::TmParams {
conditions: primer3::SolutionConditions {
dmso_conc: 5.0,
dmso_fact: 0.6,
..Default::default()
},
..Default::default()
};
let dmso_tm = primer3::calc_tm_with(seq, ¶ms).unwrap();
assert!(dmso_tm < base_tm, "DMSO should lower Tm: base={base_tm}, dmso={dmso_tm}");
let diff = base_tm - dmso_tm;
assert!(diff > 2.0 && diff < 5.0, "DMSO Tm shift: {diff}");
}
#[test]
fn tm_formamide_lowers_tm() {
let seq = "GTAAAACGACGGCCAGT";
let base_tm = primer3::calc_tm(seq).unwrap();
let params = primer3::TmParams {
conditions: primer3::SolutionConditions { formamide_conc: 1.0, ..Default::default() },
..Default::default()
};
let form_tm = primer3::calc_tm_with(seq, ¶ms).unwrap();
assert!(form_tm < base_tm, "Formamide should lower Tm: base={base_tm}, formamide={form_tm}");
}
#[test]
fn tm_salt_concentration_effects() {
let seq = "GTAAAACGACGGCCAGT";
let low_mv = primer3::calc_tm_with(
seq,
&primer3::TmParams {
conditions: primer3::SolutionConditions { mv_conc: 10.0, ..Default::default() },
..Default::default()
},
)
.unwrap();
let high_mv = primer3::calc_tm_with(
seq,
&primer3::TmParams {
conditions: primer3::SolutionConditions { mv_conc: 200.0, ..Default::default() },
..Default::default()
},
)
.unwrap();
assert!(high_mv > low_mv, "Higher salt should increase Tm: low={low_mv}, high={high_mv}");
}
#[test]
fn tm_dna_concentration_effects() {
let seq = "GTAAAACGACGGCCAGT";
let low_dna = primer3::calc_tm_with(
seq,
&primer3::TmParams {
conditions: primer3::SolutionConditions { dna_conc: 10.0, ..Default::default() },
..Default::default()
},
)
.unwrap();
let high_dna = primer3::calc_tm_with(
seq,
&primer3::TmParams {
conditions: primer3::SolutionConditions { dna_conc: 500.0, ..Default::default() },
..Default::default()
},
)
.unwrap();
assert!(
high_dna > low_dna,
"Higher DNA conc should increase Tm: low={low_dna}, high={high_dna}"
);
}
#[test]
fn hairpin_no_structure_returns_zeros() {
let result = primer3::calc_hairpin("AAAAAAAAAA").unwrap();
assert!(!result.structure_found());
assert_approx!(result.tm(), 0.0, TM_TOL, "no-hairpin Tm");
assert_approx!(result.dg(), 0.0, 1.0, "no-hairpin dG");
}
#[test]
fn homodimer_no_complement_returns_zeros() {
let result = primer3::calc_homodimer("AAAAAAAAAA").unwrap();
assert!(!result.structure_found());
assert_approx!(result.tm(), 0.0, TM_TOL, "no-homodimer Tm");
}
#[test]
fn heterodimer_no_complement_returns_zeros() {
let result = primer3::calc_heterodimer("AAAAAAAAAA", "AAAAAAAAAA").unwrap();
assert!(!result.structure_found());
assert_approx!(result.tm(), 0.0, TM_TOL, "no-heterodimer Tm");
}
#[test]
fn heterodimer_perfect_complement() {
let result = primer3::calc_heterodimer("AAAAAAAAAA", "TTTTTTTTTT").unwrap();
assert!(result.structure_found());
assert!(result.tm() > 0.0, "perfect complement should have positive Tm");
assert!(result.dg() < 0.0, "perfect complement should have negative dG");
}
#[test]
fn hairpin_custom_temperature() {
let seq = "CCCCCATCCGATCAGGGGG";
let args_25 = primer3::ThermoArgs { temp_c: 25.0, ..Default::default() };
let args_37 = primer3::ThermoArgs { temp_c: 37.0, ..Default::default() };
let r25 = primer3::calc_hairpin_with(seq, &args_25).unwrap();
let r37 = primer3::calc_hairpin_with(seq, &args_37).unwrap();
assert!(
r25.dg() < r37.dg(),
"hairpin dG at 25C ({}) should be more negative than at 37C ({})",
r25.dg(),
r37.dg()
);
assert_approx!(r25.tm(), r37.tm(), TM_TOL, "hairpin Tm should not depend on temp_c");
}
#[test]
fn hairpin_structure_output() {
let args = primer3::ThermoArgs { output_structure: true, ..Default::default() };
let with_struct = primer3::calc_hairpin_with("CCCCCATCCGATCAGGGGG", &args).unwrap();
assert!(with_struct.ascii_structure().is_some());
assert!(
!with_struct.ascii_structure().unwrap().is_empty(),
"structure string should be non-empty"
);
let without_struct = primer3::calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
assert!(without_struct.ascii_structure().is_none());
assert_approx!(with_struct.tm(), without_struct.tm(), TM_TOL, "Tm with/without structure");
}
#[test]
fn homodimer_structure_output() {
let args = primer3::ThermoArgs { output_structure: true, ..Default::default() };
let result = primer3::calc_homodimer_with("AGTCTAGTCTATCGATCG", &args).unwrap();
assert!(result.structure_found());
assert!(result.ascii_structure().is_some());
}
const TEMPLATE: &str = "\
GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGA\
ATTCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCA\
CAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGC\
CTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTT\
CCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGC\
GGGGAGAGGCGGTTTGCGTATTGGGCGCTCTTCCGCTTCCTCGCTCACTGA\
CTCGCTGCGCTCGGTCGTTCGGCTGCGGCGAGCGGTATCAGCTCACTCAAA\
GGCGGTAATACGGTTATCCACAGAATCAGGGGATAACGCAGGAAAGAACATG\
TGAGCAAAAGGCCAGCAAAAGGCCAGGAACCGTAAAAAGGCCGCGTTGCTGG\
CGTTTTTCCATAGGCTCCGCCCCCCTGACGAGCATCACAAAAATCGACGCTC";
#[test]
fn design_pick_left_only() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.task(primer3::PrimerTask::PickLeftOnly)
.pick_left_primer(true)
.pick_right_primer(false)
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(result.pairs().is_empty(), "pick_left_only should have no pairs");
assert!(!result.left_primers().is_empty(), "pick_left_only should have left primers");
for (i, primer) in result.left_primers().iter().take(3).enumerate() {
let tm = primer.tm();
assert!((57.0..=63.0).contains(&tm), "left primer {i} Tm {tm} out of [57, 63]");
assert!(!primer.sequence().is_empty(), "left primer {i} should have a sequence");
}
}
#[test]
fn design_pick_right_only() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.task(primer3::PrimerTask::PickRightOnly)
.pick_left_primer(false)
.pick_right_primer(true)
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(result.pairs().is_empty(), "pick_right_only should have no pairs");
assert!(!result.right_primers().is_empty(), "pick_right_only should have right primers");
for (i, primer) in result.right_primers().iter().take(3).enumerate() {
let tm = primer.tm();
assert!((57.0..=63.0).contains(&tm), "right primer {i} Tm {tm} out of [57, 63]");
}
}
#[test]
fn design_multiple_excluded_regions() {
let seq_args = primer3::SequenceArgs::builder()
.sequence(TEMPLATE)
.target(200, 50)
.excluded_region(80, 30)
.excluded_region(300, 30)
.build()
.unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 400)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
for (i, pair) in result.pairs().iter().enumerate() {
let lr = pair.left().position_on_template();
let rr = pair.right().position_on_template();
let overlaps_excluded = |r: &std::ops::Range<usize>| {
(r.start < 110 && r.end > 80) || (r.start < 330 && r.end > 300)
};
assert!(!overlaps_excluded(&lr), "pair {i}: left primer overlaps excluded region");
assert!(!overlaps_excluded(&rr), "pair {i}: right primer overlaps excluded region");
}
}
#[test]
fn design_multiple_targets() {
let seq_args = primer3::SequenceArgs::builder()
.sequence(TEMPLATE)
.target(100, 30)
.target(300, 30)
.build()
.unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 500)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(!result.pairs().is_empty(), "should find primers for multiple targets");
for (i, pair) in result.pairs().iter().enumerate() {
let lr = pair.left().position_on_template();
let rr = pair.right().position_on_template();
let contains_target1 = lr.start <= 100 && rr.end >= 130;
let contains_target2 = lr.start <= 300 && rr.end >= 330;
assert!(
contains_target1 || contains_target2,
"pair {i}: product ({lr:?} to {rr:?}) should contain at least one target"
);
}
}
#[test]
fn design_tight_product_size() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(100, 120)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
for (i, pair) in result.pairs().iter().enumerate() {
let size = pair.product_size();
assert!((100..=120).contains(&size), "pair {i}: product size {size} out of [100, 120]");
}
}
#[test]
fn design_multiple_product_size_ranges() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(80, 100)
.product_size_range(200, 250)
.num_return(5)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
for (i, pair) in result.pairs().iter().enumerate() {
let size = pair.product_size();
let in_range = (80..=100).contains(&size) || (200..=250).contains(&size);
assert!(in_range, "pair {i}: product size {size} not in [80-100] or [200-250]");
}
}
#[test]
fn design_check_primers_validates_given_pair() {
let seq_args = primer3::SequenceArgs::builder()
.sequence(TEMPLATE)
.left_primer("CCTGGGGTGCCTAATGAGTG")
.right_primer("TACCGCCTTTGAGTGAGCTG")
.build()
.unwrap();
let settings = primer3::PrimerSettings::builder()
.task(primer3::PrimerTask::CheckPrimers)
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert_eq!(result.num_pairs(), 1, "check_primers should return exactly 1 pair");
let pair = &result.pairs()[0];
assert_eq!(pair.left().sequence(), "CCTGGGGTGCCTAATGAGTG");
assert_eq!(pair.right().sequence(), "TACCGCCTTTGAGTGAGCTG");
assert_eq!(pair.product_size(), 221);
}
#[test]
fn design_included_region() {
let seq_args = primer3::SequenceArgs::builder()
.sequence(TEMPLATE)
.included_region(100, 200)
.build()
.unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 200)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(!result.pairs().is_empty(), "should find at least one pair");
for (i, pair) in result.pairs().iter().enumerate() {
let size = pair.product_size();
assert!((75..=200).contains(&size), "pair {i}: product size {size} outside range");
}
}
#[test]
fn design_primer_size_constraints() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(55.0)
.primer_max_tm(65.0)
.primer_opt_size(25)
.primer_min_size(22)
.primer_max_size(28)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
for (i, pair) in result.pairs().iter().enumerate() {
let left_len = pair.left().length();
let right_len = pair.right().length();
assert!(
(22..=28).contains(&left_len),
"pair {i}: left primer length {left_len} out of [22, 28]"
);
assert!(
(22..=28).contains(&right_len),
"pair {i}: right primer length {right_len} out of [22, 28]"
);
}
}
#[test]
fn design_result_stats() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
let left_stats = result.left_stats();
assert!(left_stats.considered > 0, "left stats should show primers considered");
let pair_stats = result.pair_stats();
assert!(pair_stats.considered > 0, "pair stats should show pairs considered");
}
#[test]
fn design_no_results_with_impossible_constraints() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(95.0)
.primer_min_tm(93.0)
.primer_max_tm(97.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(result.pairs().is_empty(), "impossible constraints should yield no pairs");
}
#[test]
fn design_pair_properties() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.max_diff_tm(2.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
for (i, pair) in result.pairs().iter().enumerate() {
let product_tm = pair.product_tm();
assert!(
product_tm > 60.0 && product_tm < 100.0,
"pair {i}: product Tm {product_tm} out of range"
);
let tm_diff = pair.tm_diff();
assert!(
tm_diff.abs() <= 2.0 + 0.001,
"pair {i}: Tm diff {tm_diff} exceeds max_diff_tm=2.0"
);
assert!(pair.pair_penalty() >= 0.0, "pair {i}: penalty should be non-negative");
let lr = pair.left().position_on_template();
let rr = pair.right().position_on_template();
assert_eq!(pair.product_size(), rr.end - lr.start, "pair {i}: product size mismatch");
}
}
#[test]
fn design_pairs_sorted_by_penalty() {
let seq_args =
primer3::SequenceArgs::builder().sequence(TEMPLATE).target(200, 50).build().unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(10)
.build()
.unwrap();
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
let penalties: Vec<f64> =
result.pairs().iter().map(primer3::PrimerPair::pair_penalty).collect();
for w in penalties.windows(2) {
assert!(w[0] <= w[1] + 0.001, "pairs should be sorted by penalty: {} > {}", w[0], w[1]);
}
}
#[test]
fn concurrent_tm_calculations() {
use std::thread;
let handles: Vec<_> = (0..8)
.map(|i| {
thread::spawn(move || {
let seq = match i % 4 {
0 => "GTAAAACGACGGCCAGT",
1 => "GCGCGCGCGCGCGCGCGCGC",
2 => "AAAAAAAAAAAAAAAAAAA",
_ => "ATCGATCGATCGATCGATCG",
};
for _ in 0..100 {
let tm = primer3::calc_tm(seq).unwrap();
assert!(tm > 0.0 && tm < 100.0);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn concurrent_thermo_calculations() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|i| {
thread::spawn(move || {
for _ in 0..50 {
match i % 3 {
0 => {
let _ = primer3::calc_hairpin("CCCCCATCCGATCAGGGGG").unwrap();
}
1 => {
let _ = primer3::calc_homodimer("AGTCTAGTCTATCGATCG").unwrap();
}
_ => {
let _ = primer3::calc_heterodimer("AAAAAAAAAA", "TTTTTTTTTT").unwrap();
}
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn concurrent_design() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
let seq_args = primer3::SequenceArgs::builder()
.sequence(TEMPLATE)
.target(200, 50)
.build()
.unwrap();
let settings = primer3::PrimerSettings::builder()
.primer_opt_tm(60.0)
.primer_min_tm(57.0)
.primer_max_tm(63.0)
.product_size_range(75, 300)
.num_return(3)
.build()
.unwrap();
for _ in 0..5 {
let result = primer3::design_primers(&seq_args, &settings, None, None).unwrap();
assert!(result.num_pairs() > 0);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn oligodg_basic() {
let dg = primer3::calc_oligodg("GTAAAACGACGGCCAGT").unwrap();
assert!(dg > 0.0, "expected positive dG of disruption, got {dg}");
}
#[test]
fn oligodg_gc_rich_more_stable() {
let dg_at = primer3::calc_oligodg("AAATTTAAATTT").unwrap();
let dg_gc = primer3::calc_oligodg("GGGCCCGGGCCC").unwrap();
assert!(
dg_gc > dg_at,
"GC-rich oligo should have larger disruption dG: at={dg_at}, gc={dg_gc}"
);
}
#[test]
fn oligodg_breslauer_differs() {
let dg_sl = primer3::calc_oligodg("GTAAAACGACGGCCAGT").unwrap();
let dg_br =
primer3::calc_oligodg_with("GTAAAACGACGGCCAGT", primer3::TmMethod::Breslauer).unwrap();
assert!((dg_sl - dg_br).abs() > 0.1, "methods should differ");
}
#[test]
fn oligodg_empty_error() {
assert!(primer3::calc_oligodg("").is_err());
}
#[test]
fn end_oligodg_basic() {
let dg_full = primer3::calc_oligodg("GTAAAACGACGGCCAGT").unwrap();
let dg_end5 = primer3::calc_end_oligodg("GTAAAACGACGGCCAGT", 5).unwrap();
assert!(
(dg_full - dg_end5).abs() > 0.1,
"end dG should differ from full: full={dg_full}, end5={dg_end5}"
);
}
#[test]
fn end_oligodg_len_exceeds_sequence() {
let dg_full = primer3::calc_oligodg("ACGT").unwrap();
let dg_end100 = primer3::calc_end_oligodg("ACGT", 100).unwrap();
assert_approx!(dg_full, dg_end100, 0.001, "end_oligodg with len > seq");
}
#[test]
fn end_oligodg_empty_error() {
assert!(primer3::calc_end_oligodg("", 5).is_err());
}
#[test]
fn symmetry_palindromes() {
assert!(primer3::is_symmetric("ATAT"));
assert!(primer3::is_symmetric("ACGT"));
assert!(primer3::is_symmetric("AATTAATT"));
assert!(primer3::is_symmetric("GCGC"));
}
#[test]
fn symmetry_non_palindromes() {
assert!(!primer3::is_symmetric("AAAA"));
assert!(!primer3::is_symmetric("ACGA"));
assert!(!primer3::is_symmetric("GTAAAACGACGGCCAGT"));
}
#[test]
fn symmetry_empty_and_edge_cases() {
assert!(!primer3::is_symmetric(""));
assert!(!primer3::is_symmetric("A"));
}
#[test]
fn divalent_to_monovalent_basic() {
let equiv = primer3::divalent_to_monovalent(1.5, 0.6);
assert!(equiv > 0.0, "should produce positive equivalent: {equiv}");
}
#[test]
fn divalent_to_monovalent_zero_divalent() {
let equiv = primer3::divalent_to_monovalent(0.0, 0.6);
assert!(equiv <= 0.0, "zero divalent should give non-positive: {equiv}");
}
#[test]
fn divalent_to_monovalent_high_dntp() {
let equiv = primer3::divalent_to_monovalent(1.5, 10.0);
assert!(equiv <= 0.0, "high dNTP should chelate all divalent: {equiv}");
}
#[test]
fn align_identical_sequences() {
let result =
primer3::align("ACGTACGT", "ACGTACGT", &primer3::AlignmentArgs::default()).unwrap();
assert!(result.score() > 0.0, "identical seqs should have positive score");
}
#[test]
fn align_non_matching_bases() {
let result =
primer3::align("AAAAAAAAAA", "TTTTTTTTTT", &primer3::AlignmentArgs::default()).unwrap();
assert!(result.score() <= 0.0, "non-matching bases should score ≤ 0: {}", result.score());
}
#[test]
fn align_no_similarity() {
let result =
primer3::align("AAAAAAAA", "CCCCCCCC", &primer3::AlignmentArgs::default()).unwrap();
assert!(result.score() <= 0.0, "dissimilar seqs should score ≤ 0");
}
#[test]
fn align_all_modes() {
for mode in [
primer3::AlignmentMode::Local,
primer3::AlignmentMode::Global,
primer3::AlignmentMode::GlobalEnd,
primer3::AlignmentMode::LocalEnd,
] {
let args = primer3::AlignmentArgs { mode, ..Default::default() };
let result = primer3::align("ACGTACGT", "ACGTACGT", &args).unwrap();
assert!(result.score() > 0.0, "mode {mode:?} should score > 0");
}
}
#[test]
fn align_with_full_output() {
let args =
primer3::AlignmentArgs { output: primer3::AlignmentOutput::Full, ..Default::default() };
let result = primer3::align("ACGTACGT", "ACGTACGT", &args).unwrap();
assert!(result.score() > 0.0);
assert!(result.path_length() > 0, "full mode should produce a path");
}
#[test]
fn align_custom_gap_penalty() {
let default_result =
primer3::align("ACGTNNNNACGT", "ACGTACGT", &primer3::AlignmentArgs::default()).unwrap();
let lenient_args =
primer3::AlignmentArgs { gap: -10, gap_ext: -10, max_gap: -1, ..Default::default() };
let lenient_result = primer3::align("ACGTNNNNACGT", "ACGTACGT", &lenient_args).unwrap();
assert!(
lenient_result.score() >= default_result.score(),
"lenient gap penalty should score >= strict"
);
}
#[test]
fn align_empty_error() {
assert!(primer3::align("", "ACGT", &primer3::AlignmentArgs::default()).is_err());
assert!(primer3::align("ACGT", "", &primer3::AlignmentArgs::default()).is_err());
}
#[test]
fn align_too_long_error() {
let long_seq = "A".repeat(primer3::MAX_ALIGN_LENGTH + 1);
assert!(primer3::align(&long_seq, "ACGT", &primer3::AlignmentArgs::default()).is_err());
}
#[test]
fn align_ambiguity_codes() {
let args = primer3::AlignmentArgs { use_ambiguity_codes: true, ..Default::default() };
let result = primer3::align("RRRRR", "AAAAA", &args).unwrap();
assert!(result.score() > 0.0, "R should match A with ambiguity codes");
}