solverforge-bridge 0.23.0

Public dynamic model bridge contracts for SolverForge host-language bindings
Documentation
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use solverforge_core::score::{ParseableScore, Score};

use crate::{
    active_dynamic_score_family, scoped_dynamic_score_family, DynamicScore, DynamicScoreFamily,
};

fn hash_of(score: DynamicScore) -> u64 {
    let mut hasher = DefaultHasher::new();
    score.hash(&mut hasher);
    hasher.finish()
}

#[test]
fn dynamic_score_keeps_static_three_level_score_contract() {
    let score = DynamicScore::of(-1, 2, 3);

    assert_eq!(DynamicScore::levels_count(), 3);
    assert!(!score.is_feasible());
    assert_eq!(score.to_level_numbers(), vec![-1, 2, 3]);
    assert_eq!(score.family_levels(DynamicScoreFamily::Soft), vec![3]);
    assert_eq!(
        score.family_levels(DynamicScoreFamily::HardSoft),
        vec![-1, 3]
    );
}

#[test]
fn dynamic_score_displays_the_active_family() {
    let display =
        |family, score: DynamicScore| scoped_dynamic_score_family(family, || score.to_string());

    assert_eq!(
        display(DynamicScoreFamily::Soft, DynamicScore::soft(3)),
        "3"
    );
    assert_eq!(
        display(DynamicScoreFamily::HardSoft, DynamicScore::hard_soft(-1, 3)),
        "-1hard/3soft"
    );
    assert_eq!(
        display(
            DynamicScoreFamily::HardSoftDecimal,
            DynamicScore::hard_soft(-100_000, 150_500)
        ),
        "-1hard/1.505soft"
    );
    assert_eq!(
        display(
            DynamicScoreFamily::HardMediumSoft,
            DynamicScore::of(-1, 2, 3)
        ),
        "-1hard/2medium/3soft"
    );
}

#[test]
fn dynamic_score_family_scope_restores_the_previous_family() {
    scoped_dynamic_score_family(DynamicScoreFamily::Soft, || {
        scoped_dynamic_score_family(DynamicScoreFamily::HardSoft, || {
            assert_eq!(active_dynamic_score_family(), DynamicScoreFamily::HardSoft);
        });
        assert_eq!(active_dynamic_score_family(), DynamicScoreFamily::Soft);
    });
    assert_eq!(
        active_dynamic_score_family(),
        DynamicScoreFamily::HardMediumSoft
    );
}

#[test]
fn dynamic_score_parses_standard_score_strings() {
    assert_eq!(
        DynamicScore::parse("-1hard/2medium/3soft").unwrap(),
        DynamicScore::of(-1, 2, 3)
    );
    assert_eq!(
        DynamicScore::parse("-1hard/3soft").unwrap(),
        DynamicScore::hard_soft(-1, 3)
    );
    assert_eq!(
        DynamicScore::parse("-1hard/1.505soft").unwrap(),
        DynamicScore::hard_soft(-100_000, 150_500)
    );
    assert_eq!(DynamicScore::parse("3").unwrap(), DynamicScore::soft(3));
}

#[test]
fn dynamic_score_equality_agrees_with_ordering() {
    let declared = DynamicScore::hard_soft(0, 0);

    assert_eq!(declared.cmp(&DynamicScore::ZERO), Ordering::Equal);
    assert_eq!(declared, DynamicScore::ZERO);
    assert_eq!(hash_of(declared), hash_of(DynamicScore::ZERO));
    assert!(DynamicScore::of(0, 0, 0) > DynamicScore::of(-1, 100, 100));
}

#[test]
fn dynamic_score_zero_does_not_depend_on_the_calling_thread() {
    // Evaluator replicas score on worker threads that never entered the
    // solve's family scope; their zero must equal the solve thread's.
    let solve_thread_zero = scoped_dynamic_score_family(DynamicScoreFamily::HardSoft, || {
        <DynamicScore as Score>::zero()
    });

    assert_eq!(solve_thread_zero, <DynamicScore as Score>::zero());
}

#[test]
fn dynamic_score_parse_inverts_display_for_decimal_models() {
    scoped_dynamic_score_family(DynamicScoreFamily::HardSoftDecimal, || {
        let limit = DynamicScore::parse("0hard/-5soft").unwrap();

        assert_eq!((limit.hard, limit.soft), (0, -500_000));
        assert_eq!(limit.to_string(), "0hard/-5soft");
    });
}