solverforge 0.11.1

SolverForge - A constraint solver in Rust
Documentation
// Integration tests for derive macros.

use solverforge::__internal::{PlanningId, PlanningSolution as PlanningSolutionTrait};
use solverforge::prelude::*;

#[path = "derive_macros/aliased_route_plan/mod.rs"]
mod aliased_route_plan_domain;
#[path = "derive_macros/duplicate_names/mod.rs"]
mod duplicate_names;
#[path = "derive_macros/route_plan/mod.rs"]
mod route_plan_domain;
#[path = "derive_macros/schedule/mod.rs"]
mod schedule_domain;
#[path = "derive_macros/shadow_plan/mod.rs"]
mod shadow_plan_domain;

use aliased_route_plan_domain::{AliasedRoutePlan, Route as AliasedRoute, Visit as AliasedVisit};
use route_plan_domain::{Route, RoutePlan, Visit};
use schedule_domain::{Employee, Schedule, Shift};
use shadow_plan_domain::{MultiOwnerShadowPlan, RoutedVisit, ShadowRoute, ShadowShift, ShiftVisit};

#[test]
fn test_problem_fact_derives_correctly() {
    let employee = Employee {
        id: 1,
        name: "Alice".to_string(),
    };
    assert_eq!(PlanningId::planning_id(&employee), 1);
    assert_eq!(
        Employee::problem_fact_descriptor("employees").id_field,
        Some("id")
    );
}

#[test]
fn test_planning_entity_derives_correctly() {
    let shift = Shift {
        id: 42,
        employee_id: Some(1),
    };
    assert_eq!(PlanningId::planning_id(&shift), 42);
}

#[test]
fn test_planning_solution_derives_correctly() {
    let schedule = Schedule {
        employees: vec![Employee {
            id: 1,
            name: "Alice".to_string(),
        }],
        shifts: vec![Shift {
            id: 42,
            employee_id: None,
        }],
        score: Some(HardSoftScore::of(0, 0)),
    };
    assert_eq!(
        PlanningSolutionTrait::score(&schedule),
        Some(HardSoftScore::of(0, 0))
    );

    let mut schedule2 = schedule.clone();
    PlanningSolutionTrait::set_score(&mut schedule2, Some(HardSoftScore::of(-1, -5)));
    assert_eq!(
        PlanningSolutionTrait::score(&schedule2),
        Some(HardSoftScore::of(-1, -5))
    );
}

#[test]
fn test_solution_descriptor_preserves_entity_variable_metadata() {
    let descriptor = Schedule::descriptor();
    let shift_descriptor = descriptor
        .find_entity_descriptor("Shift")
        .expect("Shift descriptor should be present");

    assert_eq!(shift_descriptor.solution_field, "shifts");
    assert_eq!(shift_descriptor.id_field, Some("id"));

    let employee_var = shift_descriptor
        .find_variable("employee_id")
        .expect("employee_id variable descriptor should be present");

    assert!(employee_var.allows_unassigned);
    assert_eq!(employee_var.value_range_provider, Some("employees"));
}

#[test]
fn test_field_only_list_solution_preserves_list_descriptor_metadata() {
    let descriptor = RoutePlan::descriptor();
    let route_descriptor = descriptor
        .find_entity_descriptor("Route")
        .expect("Route descriptor should be present");

    assert_eq!(route_descriptor.solution_field, "routes");
    assert_eq!(route_descriptor.id_field, Some("id"));

    let visits_var = route_descriptor
        .find_variable("visits")
        .expect("visits variable descriptor should be present");

    assert_eq!(visits_var.name, "visits");
    assert_eq!(
        visits_var.variable_type,
        solverforge_core::domain::VariableType::List
    );
}

#[test]
fn test_single_owner_list_helpers_remain_available_when_unambiguous() {
    let mut plan = RoutePlan {
        visits: vec![Visit { id: 10 }],
        routes: vec![Route {
            id: 1,
            visits: vec![0],
        }],
        score: None,
    };

    assert_eq!(plan.list_len(0), 1);
    assert_eq!(RoutePlan::list_len_static(&plan, 0), 1);
    assert_eq!(RoutePlan::element_count(&plan), 1);
    assert_eq!(RoutePlan::n_entities(&plan), 1);
    assert_eq!(RoutePlan::assigned_elements(&plan), vec![0]);
    assert_eq!(RoutePlan::index_to_element_static(&plan, 0), 0);
    assert_eq!(RoutePlan::list_variable_descriptor_index(), 0);

    assert_eq!(RoutePlan::routes_list_len_static(&plan, 0), 1);
    assert_eq!(RoutePlan::routes_element_count(&plan), 1);
    assert_eq!(RoutePlan::routes_n_entities(&plan), 1);
    assert_eq!(RoutePlan::routes_assigned_elements(&plan), vec![0]);
    assert_eq!(RoutePlan::routes_index_to_element_static(&plan, 0), 0);
    assert_eq!(RoutePlan::routes_list_variable_descriptor_index(), 0);

    RoutePlan::assign_element(&mut plan, 0, 0);
    RoutePlan::routes_assign_element(&mut plan, 0, 0);
    assert_eq!(RoutePlan::list_len_static(&plan, 0), 3);
}

#[test]
fn test_multi_owner_shadow_updates_are_descriptor_scoped() {
    let mut plan = MultiOwnerShadowPlan {
        routes: vec![ShadowRoute {
            id: 1,
            visits: vec![0],
        }],
        shifts: vec![ShadowShift {
            id: 2,
            visits: vec![0],
        }],
        routed_visits: vec![RoutedVisit {
            id: 10,
            route: None,
        }],
        shift_visits: vec![ShiftVisit { id: 20 }],
        score: None,
    };

    <MultiOwnerShadowPlan as PlanningSolutionTrait>::update_entity_shadows(&mut plan, 1, 0);
    assert_eq!(plan.routed_visits[0].route, None);

    <MultiOwnerShadowPlan as PlanningSolutionTrait>::update_entity_shadows(&mut plan, 0, 0);
    assert_eq!(plan.routed_visits[0].route, Some(0));

    plan.routed_visits[0].route = None;
    <MultiOwnerShadowPlan as PlanningSolutionTrait>::update_all_shadows(&mut plan);
    assert_eq!(plan.routed_visits[0].route, Some(0));
}

#[test]
fn test_multi_owner_list_helpers_are_owner_scoped() {
    let mut plan = MultiOwnerShadowPlan {
        routes: vec![ShadowRoute {
            id: 1,
            visits: vec![0],
        }],
        shifts: vec![ShadowShift {
            id: 2,
            visits: vec![0],
        }],
        routed_visits: vec![RoutedVisit {
            id: 10,
            route: None,
        }],
        shift_visits: vec![ShiftVisit { id: 20 }],
        score: None,
    };

    assert_eq!(MultiOwnerShadowPlan::routes_list_len_static(&plan, 0), 1);
    assert_eq!(MultiOwnerShadowPlan::shifts_list_len_static(&plan, 0), 1);
    assert_eq!(MultiOwnerShadowPlan::routes_element_count(&plan), 1);
    assert_eq!(MultiOwnerShadowPlan::shifts_element_count(&plan), 1);
    assert_eq!(MultiOwnerShadowPlan::routes_n_entities(&plan), 1);
    assert_eq!(MultiOwnerShadowPlan::shifts_n_entities(&plan), 1);
    assert_eq!(
        MultiOwnerShadowPlan::routes_assigned_elements(&plan),
        vec![0]
    );
    assert_eq!(
        MultiOwnerShadowPlan::shifts_assigned_elements(&plan),
        vec![0]
    );
    assert_eq!(
        MultiOwnerShadowPlan::routes_index_to_element_static(&plan, 0),
        0
    );
    assert_eq!(
        MultiOwnerShadowPlan::shifts_index_to_element_static(&plan, 0),
        0
    );
    assert_eq!(
        MultiOwnerShadowPlan::routes_list_variable_descriptor_index(),
        0
    );
    assert_eq!(
        MultiOwnerShadowPlan::shifts_list_variable_descriptor_index(),
        1
    );

    MultiOwnerShadowPlan::routes_assign_element(&mut plan, 0, 0);
    MultiOwnerShadowPlan::shifts_assign_element(&mut plan, 0, 0);
    assert_eq!(MultiOwnerShadowPlan::routes_list_len_static(&plan, 0), 2);
    assert_eq!(MultiOwnerShadowPlan::shifts_list_len_static(&plan, 0), 2);
}

#[test]
fn test_list_helpers_work_for_aliased_single_owner_types() {
    let mut plan = AliasedRoutePlan {
        visits: vec![AliasedVisit { id: 10 }],
        routes: vec![AliasedRoute {
            id: 1,
            visits: vec![0],
        }],
        score: None,
    };

    assert_eq!(AliasedRoutePlan::list_len_static(&plan, 0), 1);
    assert_eq!(AliasedRoutePlan::routes_list_len_static(&plan, 0), 1);
    assert_eq!(AliasedRoutePlan::routes_element_count(&plan), 1);

    AliasedRoutePlan::assign_element(&mut plan, 0, 0);
    AliasedRoutePlan::routes_assign_element(&mut plan, 0, 0);
    assert_eq!(AliasedRoutePlan::list_len_static(&plan, 0), 3);
}

#[test]
fn test_duplicate_short_names_do_not_confuse_list_helper_binding() {
    let plan = duplicate_names::Plan {
        visits: vec![duplicate_names::Visit { id: 10 }],
        listed_routes: vec![duplicate_names::Route {
            id: 1,
            visits: vec![0],
        }],
        plain_routes: vec![duplicate_names::RenamedPlainRoute { id: 2 }],
        score: None,
    };

    assert_eq!(duplicate_names::Plan::list_len_static(&plan, 0), 1);
    assert_eq!(
        duplicate_names::Plan::listed_routes_list_len_static(&plan, 0),
        1
    );

    let panic = std::panic::catch_unwind(|| {
        let _ = duplicate_names::Plan::plain_routes_list_len_static(&plan, 0);
    })
    .expect_err("non-list entity collections should reject list helper calls");

    let message = if let Some(message) = panic.downcast_ref::<String>() {
        message.as_str()
    } else if let Some(message) = panic.downcast_ref::<&str>() {
        message
    } else {
        ""
    };
    assert!(message.contains("plain_routes"));
}

#[test]
fn test_multi_owner_generic_list_helpers_reject_ambiguous_calls() {
    let plan = MultiOwnerShadowPlan {
        routes: vec![ShadowRoute {
            id: 1,
            visits: vec![0],
        }],
        shifts: vec![ShadowShift {
            id: 2,
            visits: vec![0],
        }],
        routed_visits: vec![RoutedVisit {
            id: 10,
            route: None,
        }],
        shift_visits: vec![ShiftVisit { id: 20 }],
        score: None,
    };

    let panic = std::panic::catch_unwind(|| {
        let _ = MultiOwnerShadowPlan::list_len_static(&plan, 0);
    })
    .expect_err("multi-owner plans should reject generic list helper calls");

    let message = if let Some(message) = panic.downcast_ref::<String>() {
        message.as_str()
    } else if let Some(message) = panic.downcast_ref::<&str>() {
        message
    } else {
        ""
    };
    assert!(message.contains("single-owner list helper"));
}