"""Tests for OR-Tools solver implementation."""
import pytest
from chuk_mcp_solver.models import SolveConstraintModelRequest, SolverStatus
from chuk_mcp_solver.solver.ortools import ORToolsSolver
# ============================================================================
# OR-Tools Provider Tests - Simple Cases
# ============================================================================
@pytest.mark.asyncio
async def test_simple_satisfy():
"""Test simple satisfaction problem."""
request = SolveConstraintModelRequest(
mode="satisfy",
variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 10}}],
constraints=[
{
"id": "bound",
"kind": "linear",
"params": {"terms": [{"var": "x", "coef": 1}], "sense": ">=", "rhs": 5},
}
],
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
assert len(response.solutions) == 1
assert len(response.solutions[0].variables) == 1
assert response.solutions[0].variables[0].value >= 5
@pytest.mark.asyncio
async def test_simple_optimize():
"""Test simple optimization problem."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 10}}],
constraints=[],
objective={"sense": "min", "terms": [{"var": "x", "coef": 1}]},
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
assert response.objective_value == 0.0
assert response.solutions[0].variables[0].value == 0
@pytest.mark.asyncio
async def test_knapsack_problem(simple_knapsack_request):
"""Test knapsack optimization."""
request = SolveConstraintModelRequest(**simple_knapsack_request)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
assert response.objective_value is not None
# Should take item_2 (weight 5, value 15) and item_3 (weight 2, value 8)
# Total weight: 7 <= 7, Total value: 23
assert response.objective_value == 23.0
@pytest.mark.asyncio
async def test_all_different_constraint(simple_sudoku_cell_request):
"""Test all_different constraint."""
request = SolveConstraintModelRequest(**simple_sudoku_cell_request)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
# Extract values
values = {var.id: int(var.value) for var in response.solutions[0].variables}
# cell_0 should be 1 (fixed)
assert values["cell_0"] == 1
# All should be different and in range [1, 4]
assert len(set(values.values())) == 4
assert all(1 <= v <= 4 for v in values.values())
@pytest.mark.asyncio
async def test_infeasible_problem(infeasible_request):
"""Test infeasible problem."""
request = SolveConstraintModelRequest(**infeasible_request)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.INFEASIBLE
assert len(response.solutions) == 0
assert response.explanation is not None
assert "infeasible" in response.explanation.summary.lower()
# ============================================================================
# Advanced Constraint Tests
# ============================================================================
@pytest.mark.asyncio
async def test_element_constraint():
"""Test element constraint."""
request = SolveConstraintModelRequest(
mode="satisfy",
variables=[
{"id": "index", "domain": {"type": "integer", "lower": 0, "upper": 2}},
{"id": "result", "domain": {"type": "integer", "lower": 0, "upper": 100}},
],
constraints=[
{
"id": "elem",
"kind": "element",
"params": {"index_var": "index", "array": [10, 20, 30], "target_var": "result"},
},
{
"id": "index_fixed",
"kind": "linear",
"params": {"terms": [{"var": "index", "coef": 1}], "sense": "==", "rhs": 1},
},
],
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
values = {var.id: int(var.value) for var in response.solutions[0].variables}
assert values["index"] == 1
assert values["result"] == 20 # array[1]
@pytest.mark.asyncio
async def test_table_constraint():
"""Test table constraint."""
request = SolveConstraintModelRequest(
mode="satisfy",
variables=[
{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 2}},
{"id": "y", "domain": {"type": "integer", "lower": 0, "upper": 2}},
],
constraints=[
{
"id": "table",
"kind": "table",
"params": {"vars": ["x", "y"], "allowed_tuples": [[0, 1], [1, 0], [2, 2]]},
}
],
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
values = {var.id: int(var.value) for var in response.solutions[0].variables}
assert (values["x"], values["y"]) in [(0, 1), (1, 0), (2, 2)]
@pytest.mark.asyncio
async def test_implication_constraint():
"""Test implication constraint."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[
{"id": "use_feature", "domain": {"type": "bool"}},
{"id": "cost", "domain": {"type": "integer", "lower": 0, "upper": 20}},
],
constraints=[
{
"id": "impl",
"kind": "implication",
"params": {
"if_var": "use_feature",
"then": {
"id": "cost_req",
"kind": "linear",
"params": {
"terms": [{"var": "cost", "coef": 1}],
"sense": ">=",
"rhs": 10,
},
},
},
}
],
objective={"sense": "min", "terms": [{"var": "cost", "coef": 1}]},
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
# Should set use_feature=0 to avoid cost requirement
values = {var.id: var.value for var in response.solutions[0].variables}
assert values["use_feature"] == 0
assert values["cost"] == 0
# ============================================================================
# Boolean Variable Tests
# ============================================================================
@pytest.mark.asyncio
async def test_boolean_variables():
"""Test boolean variable handling."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[
{"id": "x", "domain": {"type": "bool"}},
{"id": "y", "domain": {"type": "bool"}},
],
constraints=[
{
"id": "at_least_one",
"kind": "linear",
"params": {
"terms": [{"var": "x", "coef": 1}, {"var": "y", "coef": 1}],
"sense": ">=",
"rhs": 1,
},
}
],
objective={"sense": "min", "terms": [{"var": "x", "coef": 1}, {"var": "y", "coef": 1}]},
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
# Should set exactly one to 1
values = {var.id: var.value for var in response.solutions[0].variables}
assert sum(values.values()) == 1
# ============================================================================
# Metadata and Explanation Tests
# ============================================================================
@pytest.mark.asyncio
async def test_variable_metadata_preserved():
"""Test that variable metadata is preserved in solution."""
request = SolveConstraintModelRequest(
mode="satisfy",
variables=[
{
"id": "task_A",
"domain": {"type": "bool"},
"metadata": {"task_name": "Task A", "priority": 1},
}
],
constraints=[],
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
sol_var = response.solutions[0].variables[0]
assert sol_var.metadata == {"task_name": "Task A", "priority": 1}
@pytest.mark.asyncio
async def test_binding_constraints_identified():
"""Test that binding constraints are identified."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 10}}],
constraints=[
{
"id": "upper_limit",
"kind": "linear",
"params": {"terms": [{"var": "x", "coef": 1}], "sense": "<=", "rhs": 5},
"metadata": {"description": "Maximum capacity"},
}
],
objective={"sense": "max", "terms": [{"var": "x", "coef": 1}]},
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
assert response.explanation is not None
assert len(response.explanation.binding_constraints) > 0
assert response.explanation.binding_constraints[0].id == "upper_limit"
# ============================================================================
# Search Configuration Tests
# ============================================================================
@pytest.mark.asyncio
async def test_search_config_timeout():
"""Test search configuration with timeout."""
# Create a problem that would take time to solve optimally
request = SolveConstraintModelRequest(
mode="optimize",
variables=[
{"id": f"x{i}", "domain": {"type": "integer", "lower": 0, "upper": 100}}
for i in range(10)
],
constraints=[],
objective={
"sense": "max",
"terms": [{"var": f"x{i}", "coef": i + 1} for i in range(10)],
},
search={"max_time_ms": 1}, # Very short timeout
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
# Should complete quickly, status may be optimal or timeout depending on speed
assert response.status in (SolverStatus.OPTIMAL, SolverStatus.TIMEOUT, SolverStatus.FEASIBLE)
# ============================================================================
# Validation Tests
# ============================================================================
@pytest.mark.asyncio
async def test_optimize_without_objective_fails():
"""Test that optimize mode without objective fails validation."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[{"id": "x", "domain": {"type": "bool"}}],
constraints=[],
)
solver = ORToolsSolver()
# Now handled by validation framework, returns error response instead of raising
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.ERROR
assert "objective" in response.explanation.summary.lower()
# ============================================================================
# Edge Cases
# ============================================================================
@pytest.mark.asyncio
async def test_no_constraints():
"""Test problem with no constraints."""
request = SolveConstraintModelRequest(
mode="optimize",
variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 10}}],
constraints=[],
objective={"sense": "max", "terms": [{"var": "x", "coef": 1}]},
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.OPTIMAL
assert response.solutions[0].variables[0].value == 10 # Maximum value
@pytest.mark.asyncio
async def test_single_variable_single_constraint():
"""Test minimal problem."""
request = SolveConstraintModelRequest(
mode="satisfy",
variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 5}}],
constraints=[
{
"id": "fixed",
"kind": "linear",
"params": {"terms": [{"var": "x", "coef": 1}], "sense": "==", "rhs": 3},
}
],
)
solver = ORToolsSolver()
response = await solver.solve_constraint_model(request)
assert response.status == SolverStatus.SATISFIED
assert response.solutions[0].variables[0].value == 3