CoolFace
Apppublic

gl29/kervent-projections

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
test_export_models.py656 linesDownload Raw Back to tests
1# tests/test_export_models.py2"""Test suite for model configuration export functionality.3 4Verifies that all model configurations (yurt, bnb, etc.) are properly5exported to JSON with complete structure validation.6"""7 8import json9import copy10 11import numpy as np12import pytest13 14from core import EntitySimulationResult, GlobalConfig15from export import (16    _flatten_inputs,17    generate_json_export,18    _numpy_to_python,19)20from ledger import (21    BaseLedgerConfig,22    CapExItem,23    CapExLedger,24    OpExItem,25    OpExLedger,26    RevenueModel,27    RevenueStream,28    TaxModel,29    TaxTables,30    Phase,31    SCIEntity,32    OpCoEntity,33    PersonEntity,34    DebtServiceModel,35)36 37 38# ==========================================39# FIXTURES40# ==========================================41 42 43@pytest.fixture44def mock_global_cfg():45    """Create a minimal global config."""46    return GlobalConfig(47        inflation_rate=0.02,48        vat_franchise_active=False,49        tax_tables=TaxTables.from_dict({}),50    )51 52 53@pytest.fixture54def mock_config():55    """Create a minimal KerventConfig-like object."""56 57    class MockKerventConfig:58        def __init__(self):59            self.capex = CapExLedger()60            self.opex = OpExLedger()61 62        def to_dict(self):63            return {"capex": self.capex.to_dict(), "opex": self.opex.to_dict()}64 65    return MockKerventConfig()66 67 68@pytest.fixture69def mock_sci():70    """Create a minimal SCIEntity."""71    return SCIEntity(72        monthly_rent=1000.0,73        rent_mode="fixed",74        dividend_reserve=5000.0,75        tax_regime="is",76    )77 78 79@pytest.fixture80def mock_opco():81    """Create a minimal OpCoEntity."""82    return OpCoEntity(dividend_reserve=5000.0)83 84 85@pytest.fixture86def mock_persons():87    """Create a list of minimal PersonEntity objects."""88    return [89        PersonEntity(90            name="Alice",91            min_monthly_salary=2000.0,92            target_monthly_salary=3000.0,93            external_monthly_income=0.0,94            ir_parts=1.0,95        ),96    ]97 98 99@pytest.fixture100def mock_phases():101    """Create a minimal phase list."""102    return [103        Phase(104            id="phase_1",105            name="Phase 1",106            trigger={"type": "immediate"},107            active_models=["yurt_model"],108            new_loan=DebtServiceModel(bank_pct=0.8, interest=0.04, loan_years=15),109            loan_entity="OpCo",110            model_configs={},111        )112    ]113 114 115@pytest.fixture116def mock_result():117    """Create a mock EntitySimulationResult."""118    max_t = 60119    opco_capex = np.zeros(max_t)120    opco_capex[0] = 10000121 122    return EntitySimulationResult(123        sci_treasury=np.linspace(100000, 200000, max_t),124        opco_treasury=np.linspace(50000, 150000, max_t),125        person_treasuries={"Alice": np.linspace(30000, 50000, max_t)},126        consolidated_treasury=np.linspace(180000, 350000, max_t),127        annual_sci_tax=5000.0,128        annual_opco_tax=3000.0,129        annual_person_tax={"Alice": 2000.0},130        opco_revenue=np.linspace(20000, 30000, max_t),131        opco_opex=np.linspace(5000, 6000, max_t),132        opco_social_charges=np.linspace(3000, 4000, max_t),133        sci_debt=np.linspace(100000, 50000, max_t),134        sci_interest=np.linspace(2000, 1000, max_t),135        sci_depreciation=np.ones(max_t) * 500,136        vat_net=np.linspace(1000, 2000, max_t),137        opco_capex=opco_capex,138        opco_loan_payments=np.linspace(2000, 2000, max_t),139        opco_pre_salary_treasury=np.linspace(5000, 15000, max_t),140        minimum_required_cash=150000.0,141        dscr=1.2,142        break_even_pct=50.0,143        break_even_idx=24,144        year_1_dividends=5000.0,145        phase_trigger_months=[24],146    )147 148 149@pytest.fixture150def yurt_model():151    """Create a yurt model config."""152    return {153        "name": "๐Ÿ•๏ธ Yourtes",154        "icon": "๐Ÿ•๏ธ",155        "default_active": True,156        "config": BaseLedgerConfig(157            revenue=RevenueModel(158                streams=[159                    RevenueStream(160                        name="Nightly rate",161                        price=100.0,162                        units=30,163                        vat_rate=0.10,164                    )165                ]166            ),167            capex=CapExLedger(168                items=[169                    CapExItem(170                        name="Yurt structure",171                        amount=25000.0,172                        start_delay=0,173                        count=1,174                        useful_life_years=20,175                        vat_rate=0.20,176                    )177                ]178            ),179            opex=OpExLedger(180                items=[181                    OpExItem(182                        name="Cleaning",183                        monthly_amount=500.0,184                        inflates=True,185                    )186                ]187            ),188            tax=TaxModel(),189        ),190    }191 192 193@pytest.fixture194def bnb_model():195    """Create a B&B model config."""196    return {197        "name": "๐Ÿ›๏ธ Chambre d'Hรดtes",198        "icon": "๐Ÿ›๏ธ",199        "default_active": False,200        "config": BaseLedgerConfig(201            revenue=RevenueModel(202                streams=[203                    RevenueStream(204                        name="Room rental",205                        price=80.0,206                        units=20,207                        vat_rate=0.10,208                    ),209                    RevenueStream(210                        name="Breakfast",211                        price=15.0,212                        units=20,213                        vat_rate=0.10,214                    ),215                ]216            ),217            capex=CapExLedger(items=[]),218            opex=OpExLedger(219                items=[220                    OpExItem(221                        name="Maintenance",222                        monthly_amount=300.0,223                        inflates=True,224                    )225                ]226            ),227            tax=TaxModel(),228        ),229    }230 231 232@pytest.fixture233def multi_model_dict(yurt_model, bnb_model):234    """Create a multi-model dictionary (simulates st.session_state.models)."""235    return {236        "yurt_model": yurt_model,237        "bnb_model": bnb_model,238    }239 240 241# ==========================================242# STRUCTURE VALIDATION TESTS243# ==========================================244 245 246def test_flatten_inputs_with_no_models(247    mock_sci, mock_opco, mock_persons, mock_phases, mock_global_cfg248):249    """Test that models_library is empty when no models provided."""250    inputs = _flatten_inputs(251        mock_sci,252        mock_opco,253        mock_persons,254        mock_phases,255        mock_global_cfg,256        models=None,257    )258 259    assert "models_library" in inputs260    assert inputs["models_library"] == {}261 262 263def test_flatten_inputs_with_empty_models_dict(264    mock_sci, mock_opco, mock_persons, mock_phases, mock_global_cfg265):266    """Test that models_library is empty when empty dict provided."""267    inputs = _flatten_inputs(268        mock_sci,269        mock_opco,270        mock_persons,271        mock_phases,272        mock_global_cfg,273        models={},274    )275 276    assert "models_library" in inputs277    assert inputs["models_library"] == {}278 279 280def test_flatten_inputs_with_single_model(281    mock_sci,282    mock_opco,283    mock_persons,284    mock_phases,285    mock_global_cfg,286    yurt_model,287):288    """Test that single model is properly exported."""289    models = {"yurt_model": yurt_model}290    inputs = _flatten_inputs(291        mock_sci,292        mock_opco,293        mock_persons,294        mock_phases,295        mock_global_cfg,296        models=models,297    )298 299    assert "models_library" in inputs300    assert "yurt_model" in inputs["models_library"]301 302    exported_model = inputs["models_library"]["yurt_model"]303    assert exported_model["name"] == "๐Ÿ•๏ธ Yourtes"304    assert exported_model["icon"] == "๐Ÿ•๏ธ"305    assert exported_model["default_active"] is True306    assert "config" in exported_model307 308 309def test_flatten_inputs_with_multiple_models(310    mock_sci,311    mock_opco,312    mock_persons,313    mock_phases,314    mock_global_cfg,315    multi_model_dict,316):317    """Test that all models are properly exported."""318    inputs = _flatten_inputs(319        mock_sci,320        mock_opco,321        mock_persons,322        mock_phases,323        mock_global_cfg,324        models=multi_model_dict,325    )326 327    assert "models_library" in inputs328    assert len(inputs["models_library"]) == 2329    assert "yurt_model" in inputs["models_library"]330    assert "bnb_model" in inputs["models_library"]331 332 333def test_model_library_structure_single_model(334    mock_sci,335    mock_opco,336    mock_persons,337    mock_phases,338    mock_global_cfg,339    yurt_model,340):341    """Test that model structure contains all required fields."""342    models = {"yurt_model": yurt_model}343    inputs = _flatten_inputs(344        mock_sci,345        mock_opco,346        mock_persons,347        mock_phases,348        mock_global_cfg,349        models=models,350    )351 352    model_lib = inputs["models_library"]353    assert "yurt_model" in model_lib354 355    model = model_lib["yurt_model"]356    # Required fields357    assert "name" in model358    assert "icon" in model359    assert "default_active" in model360    assert "config" in model361 362    # Verify types363    assert isinstance(model["name"], str)364    assert isinstance(model["icon"], str)365    assert isinstance(model["default_active"], bool)366    assert isinstance(model["config"], dict)367 368 369def test_model_config_structure(370    mock_sci,371    mock_opco,372    mock_persons,373    mock_phases,374    mock_global_cfg,375    yurt_model,376):377    """Test that model config has all revenue/capex/opex/tax."""378    models = {"yurt_model": yurt_model}379    inputs = _flatten_inputs(380        mock_sci,381        mock_opco,382        mock_persons,383        mock_phases,384        mock_global_cfg,385        models=models,386    )387 388    config = inputs["models_library"]["yurt_model"]["config"]389 390    # All config sections must be present391    assert "revenue" in config392    assert "capex" in config393    assert "opex" in config394    assert "tax" in config395 396    # All should be dicts397    assert isinstance(config["revenue"], dict)398    assert isinstance(config["capex"], dict)399    assert isinstance(config["opex"], dict)400    assert isinstance(config["tax"], dict)401 402 403def test_model_config_revenue_structure(404    mock_sci,405    mock_opco,406    mock_persons,407    mock_phases,408    mock_global_cfg,409    yurt_model,410):411    """Test that revenue streams are properly serialized."""412    models = {"yurt_model": yurt_model}413    inputs = _flatten_inputs(414        mock_sci,415        mock_opco,416        mock_persons,417        mock_phases,418        mock_global_cfg,419        models=models,420    )421 422    revenue = inputs["models_library"]["yurt_model"]["config"]["revenue"]423 424    # Should have streams425    assert "streams" in revenue426    assert isinstance(revenue["streams"], list)427    assert len(revenue["streams"]) > 0428 429    # Each stream should have required fields430    stream = revenue["streams"][0]431    assert "name" in stream432    assert "price" in stream433    assert "units" in stream434 435 436def test_model_config_capex_structure(437    mock_sci,438    mock_opco,439    mock_persons,440    mock_phases,441    mock_global_cfg,442    yurt_model,443):444    """Test that CapEx items are properly serialized."""445    models = {"yurt_model": yurt_model}446    inputs = _flatten_inputs(447        mock_sci,448        mock_opco,449        mock_persons,450        mock_phases,451        mock_global_cfg,452        models=models,453    )454 455    capex = inputs["models_library"]["yurt_model"]["config"]["capex"]456 457    # Should have items list458    assert "items" in capex459    assert isinstance(capex["items"], list)460    assert len(capex["items"]) > 0461 462    # Each item should have required fields463    item = capex["items"][0]464    assert "name" in item465    assert "amount" in item466 467 468def test_model_config_opex_structure(469    mock_sci,470    mock_opco,471    mock_persons,472    mock_phases,473    mock_global_cfg,474    yurt_model,475):476    """Test that OpEx items are properly serialized."""477    models = {"yurt_model": yurt_model}478    inputs = _flatten_inputs(479        mock_sci,480        mock_opco,481        mock_persons,482        mock_phases,483        mock_global_cfg,484        models=models,485    )486 487    opex = inputs["models_library"]["yurt_model"]["config"]["opex"]488 489    # Should have items list490    assert "items" in opex491    assert isinstance(opex["items"], list)492    assert len(opex["items"]) > 0493 494    # Each item should have required fields495    item = opex["items"][0]496    assert "name" in item497    assert "monthly_amount" in item498 499 500def test_all_models_exported(501    mock_sci,502    mock_opco,503    mock_persons,504    mock_phases,505    mock_global_cfg,506    multi_model_dict,507):508    """Test that every model in the dict is exported."""509    inputs = _flatten_inputs(510        mock_sci,511        mock_opco,512        mock_persons,513        mock_phases,514        mock_global_cfg,515        models=multi_model_dict,516    )517 518    # Every key in multi_model_dict should be in models_library519    for model_id in multi_model_dict.keys():520        assert model_id in inputs["models_library"]521 522 523def test_generate_json_export_with_models(524    mock_sci,525    mock_opco,526    mock_persons,527    mock_phases,528    mock_result,529    mock_global_cfg,530    multi_model_dict,531):532    """Test that JSON export includes models_library."""533    json_content = generate_json_export(534        mock_sci,535        mock_opco,536        mock_persons,537        mock_phases,538        mock_result,539        mock_global_cfg,540        include_inputs=True,541        include_outputs=True,542        scenario_name="Test with Models",543        models=multi_model_dict,544    )545 546    assert "metadata" in json_content547    assert "parameters" in json_content548    assert "monthly_data" in json_content549    assert "annual_summary" in json_content550 551    # Models should be in parameters552    params = json_content["parameters"]553    assert "models_library" in params554    assert len(params["models_library"]) == 2555 556 557def test_json_export_is_serializable(558    mock_sci,559    mock_opco,560    mock_persons,561    mock_phases,562    mock_result,563    mock_global_cfg,564    multi_model_dict,565):566    """Test that JSON export can be converted to JSON string."""567    json_content = generate_json_export(568        mock_sci,569        mock_opco,570        mock_persons,571        mock_phases,572        mock_result,573        mock_global_cfg,574        include_inputs=True,575        include_outputs=True,576        models=multi_model_dict,577    )578 579    # Should be JSON-serializable580    json_str = json.dumps(json_content, indent=2, default=str)581    assert isinstance(json_str, str)582    assert len(json_str) > 0583 584    # Should parse back585    reparsed = json.loads(json_str)586    assert "parameters" in reparsed587    assert "models_library" in reparsed["parameters"]588 589 590def test_model_metadata_preserved(591    mock_sci,592    mock_opco,593    mock_persons,594    mock_phases,595    mock_global_cfg,596    yurt_model,597    bnb_model,598):599    """Test that model name/icon/default_active are preserved exactly."""600    models = {"yurt_model": yurt_model, "bnb_model": bnb_model}601    inputs = _flatten_inputs(602        mock_sci,603        mock_opco,604        mock_persons,605        mock_phases,606        mock_global_cfg,607        models=models,608    )609 610    # Verify yurt metadata611    yurt_exported = inputs["models_library"]["yurt_model"]612    assert yurt_exported["name"] == yurt_model["name"]613    assert yurt_exported["icon"] == yurt_model["icon"]614    assert yurt_exported["default_active"] == yurt_model["default_active"]615 616    # Verify bnb metadata617    bnb_exported = inputs["models_library"]["bnb_model"]618    assert bnb_exported["name"] == bnb_model["name"]619    assert bnb_exported["icon"] == bnb_model["icon"]620    assert bnb_exported["default_active"] == bnb_model["default_active"]621 622 623def test_numpy_conversion_in_models(624    mock_sci,625    mock_opco,626    mock_persons,627    mock_phases,628    mock_global_cfg,629    yurt_model,630):631    """Test that NumPy types in model configs are converted to Python types."""632    # Manually inject numpy values to test conversion633    test_model = copy.deepcopy(yurt_model)634    # The BaseLedgerConfig already converts through to_dict, so this verifies635    # the _numpy_to_python function works on the converted output636 637    models = {"yurt_model": test_model}638    inputs = _flatten_inputs(639        mock_sci,640        mock_opco,641        mock_persons,642        mock_phases,643        mock_global_cfg,644        models=models,645    )646 647    # Verify all numeric values are Python types, not NumPy648    model_lib = inputs["models_library"]["yurt_model"]649    json_str = json.dumps(model_lib, default=str)650    # Should not raise an error651    assert isinstance(json_str, str)652 653 654if __name__ == "__main__":655    pytest.main([__file__, "-v"])656