use chematic::smiles::parse;
use proptest::prelude::*;
use yomitoki::{AnalysisConfig, Strictness, analyze};
fn linear_chain_smiles(n: usize) -> String {
"C".repeat(n.max(1))
}
fn simple_ring_smiles(n: usize) -> String {
format!("C1{}1", "C".repeat(n.saturating_sub(1)))
}
fn molecule_smiles_strategy() -> impl Strategy<Value = String> {
prop_oneofC(=O)O", "OC1CC2(C(N)C(O)C(Cl)C(N)C)CCC1C2", "C1CCC2(CC1)CCCC2", "CC(=O)[O-].C[NH3+]", ]
.as_slice()
)
.prop_map(String::from),
]
}
fn strictness_strategy() -> impl Strategy<Value = Strictness> {
prop_oneof![
Just(Strictness::Lenient),
Just(Strictness::Standard),
Just(Strictness::Strict),
]
}
fn assert_probability_like(value: f64, label: &str) {
assert!(value.is_finite(), "{label} is not finite: {value}");
assert!(
(0.0..=1.0).contains(&value),
"{label} out of 0.0..=1.0 range: {value}"
);
}
proptest! {
#[test]
fn analyze_never_panics_and_stays_in_contract(
smiles in molecule_smiles_strategy(),
strictness in strictness_strategy(),
max_heavy_atoms in 1usize..200,
) {
let Ok(mol) = parse(&smiles) else {
return Ok(());
};
let mut config = AnalysisConfig::default();
config.strictness = strictness;
config.max_heavy_atoms = max_heavy_atoms;
let report = analyze(&mol, &config).expect("a parsed molecule always yields Ok");
assert_probability_like(report.overall.difficulty.value(), "difficulty");
assert_probability_like(report.overall.synthesizability.value(), "synthesizability");
assert_probability_like(report.overall.confidence.value(), "confidence");
for contribution in &report.dominant_penalties {
assert_probability_like(contribution.contribution.value(), "contribution");
}
for score in [
&report.components.size_topology,
&report.components.ring_topology,
&report.components.stereochemical_burden,
&report.components.functional_group_liability,
&report.components.input_quality,
]
.into_iter()
.flatten()
{
prop_assert!(score.raw.is_finite(), "component raw is not finite: {}", score.raw);
assert_probability_like(score.normalized.value(), "component normalized");
assert_probability_like(score.confidence.value(), "component confidence");
prop_assert!(
score.contribution.is_finite(),
"component contribution is not finite: {}",
score.contribution
);
assert_probability_like(score.contribution, "component contribution");
}
let atom_count = mol.atom_count() as u32;
for finding in &report.findings {
for atom in &finding.atoms {
prop_assert!(
atom.0 < atom_count,
"finding {:?} references atom {} but molecule only has {atom_count} atoms",
finding.code,
atom.0
);
}
}
for suggestion in &report.suggestions {
assert_probability_like(suggestion.confidence.value(), "suggestion confidence");
for atom in &suggestion.target_atoms {
prop_assert!(
atom.0 < atom_count,
"suggestion {:?} references atom {} but molecule only has {atom_count} atoms",
suggestion.code,
atom.0
);
}
}
}
}