simulator / tests /test_config.py
mathiassandnes's picture
Claude Opus 4.5
Add test suite, CI/CD, and development workflow
85fcbc2
Raw History Blame Contribute Delete
6.04 kB
"""Tests for product configuration validation."""
import pytest
# Required top-level sections
REQUIRED_SECTIONS = ["product", "design", "costs", "pricing", "return_assumptions", "environmental"]
# Required product fields
REQUIRED_PRODUCT_FIELDS = ["name", "category", "retail_price"]
# Required design fields
REQUIRED_DESIGN_FIELDS = ["lifetime_years", "use_cycle_years", "max_cycles"]
# Required cost fields
REQUIRED_COST_FIELDS = ["manufacturing_unit_cost"]
class TestConfigStructure:
"""Test that configs have required structure."""
def test_besafe_has_required_sections(self, besafe_config):
"""BeSafe config has all required sections."""
for section in REQUIRED_SECTIONS:
assert section in besafe_config, f"Missing section: {section}"
def test_hamax_has_required_sections(self, hamax_config):
"""Hamax config has all required sections."""
for section in REQUIRED_SECTIONS:
assert section in hamax_config, f"Missing section: {section}"
def test_all_configs_have_required_sections(self, all_configs):
"""All product configs have required sections."""
for name, config in all_configs.items():
for section in REQUIRED_SECTIONS:
assert section in config, f"{name}: Missing section: {section}"
class TestProductSection:
"""Test product section validation."""
def test_product_has_required_fields(self, all_configs):
"""Product section has required fields."""
for name, config in all_configs.items():
for field in REQUIRED_PRODUCT_FIELDS:
assert field in config["product"], f"{name}: Missing product.{field}"
def test_retail_price_positive(self, all_configs):
"""Retail price is positive."""
for name, config in all_configs.items():
price = config["product"]["retail_price"]
assert price > 0, f"{name}: retail_price must be positive, got {price}"
class TestDesignSection:
"""Test design section validation."""
def test_design_has_required_fields(self, all_configs):
"""Design section has required fields."""
for name, config in all_configs.items():
for field in REQUIRED_DESIGN_FIELDS:
assert field in config["design"], f"{name}: Missing design.{field}"
def test_lifetime_greater_than_use_cycle(self, all_configs):
"""Design lifetime >= use cycle years."""
for name, config in all_configs.items():
lifetime = config["design"]["lifetime_years"]
use_cycle = config["design"]["use_cycle_years"]
assert lifetime >= use_cycle, f"{name}: lifetime_years ({lifetime}) < use_cycle_years ({use_cycle})"
def test_max_cycles_positive(self, all_configs):
"""Max cycles is positive."""
for name, config in all_configs.items():
cycles = config["design"]["max_cycles"]
assert cycles > 0, f"{name}: max_cycles must be positive, got {cycles}"
class TestCostsSection:
"""Test costs section validation."""
def test_manufacturing_cost_positive(self, all_configs):
"""Manufacturing cost is positive."""
for name, config in all_configs.items():
cost = config["costs"]["manufacturing_unit_cost"]
assert cost > 0, f"{name}: manufacturing_unit_cost must be positive, got {cost}"
def test_refurbishment_costs_exist(self, all_configs):
"""Refurbishment costs exist for grades A, B, C."""
for name, config in all_configs.items():
refurb = config["costs"].get("refurbishment", {})
assert "grade_a" in refurb, f"{name}: Missing refurbishment.grade_a"
assert "grade_b" in refurb, f"{name}: Missing refurbishment.grade_b"
def test_refurb_costs_ascending(self, all_configs):
"""Refurbishment costs: grade_a <= grade_b."""
for name, config in all_configs.items():
refurb = config["costs"]["refurbishment"]
assert refurb["grade_a"] <= refurb["grade_b"], \
f"{name}: grade_a cost ({refurb['grade_a']}) > grade_b ({refurb['grade_b']})"
class TestReturnAssumptions:
"""Test return assumptions validation."""
def test_return_rate_valid_range(self, all_configs):
"""Return rate is between 0 and 1."""
for name, config in all_configs.items():
rate = config["return_assumptions"]["return_rate"]
assert 0 <= rate <= 1, f"{name}: return_rate must be 0-1, got {rate}"
def test_grade_distribution_sums_to_one(self, all_configs):
"""Grade distribution (a + b + c) sums to 1."""
for name, config in all_configs.items():
dist = config["return_assumptions"]["grade_distribution"]
total = dist.get("a", 0) + dist.get("b", 0) + dist.get("c", 0)
assert abs(total - 1.0) < 0.01, f"{name}: grade distribution sums to {total}, not 1.0"
class TestMaterialsBOM:
"""Test materials BOM validation (if present)."""
def test_bom_weights_positive(self, all_configs):
"""BOM component weights are positive."""
for name, config in all_configs.items():
if "materials" in config and "bom" in config["materials"]:
for comp_name, comp in config["materials"]["bom"].items():
weight = comp.get("weight_kg", 0)
assert weight > 0, f"{name}: {comp_name} weight must be positive"
def test_recyclable_materials_have_recovery_value(self, all_configs):
"""Recyclable materials should have recovery value."""
for name, config in all_configs.items():
if "materials" in config and "bom" in config["materials"]:
for comp_name, comp in config["materials"]["bom"].items():
if comp.get("recyclable", False) and "virgin_cost_per_kg" in comp:
assert "recovery_value_per_kg" in comp, \
f"{name}: {comp_name} is recyclable but missing recovery_value_per_kg"