- Pydantic schemas: SolarConfig, WindConfig, BessConfig, GenerationResult - Catalog: synthetic 8760h profiles for RJ/KA/GJ solar and wind; LRU-cached loader - generation/solar.py: 25yr hourly simulation (DC losses, inverter, clipping, soiling, degradation) - generation/wind.py: power-law shear, piecewise power curve, wake/electrical losses, degradation - generation/bess_state.py: SOH-based capacity degradation with augmentation schedule - CLI: remodel --input scenario.json --output gen.parquet (Typer upgraded to 0.25.1 for Click 8.3 compat) - 43 unit tests, 97.4% coverage; mypy strict + ruff clean - S1-T10 parity gate: placeholder fixture + skipped integration tests (awaiting Nagasamudra data) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.1 KiB
Python
77 lines
2.1 KiB
Python
"""CLI smoke tests."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from remodel_engine.cli import app
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
@pytest.fixture()
|
|
def solar_scenario(tmp_path: Path) -> Path:
|
|
data = {
|
|
"solar": {
|
|
"location_id": "RJ",
|
|
"capacity_dc_mwp": 10.0,
|
|
"capacity_ac_mw": 8.0,
|
|
}
|
|
}
|
|
p = tmp_path / "scenario.json"
|
|
p.write_text(json.dumps(data))
|
|
return p
|
|
|
|
|
|
@pytest.fixture()
|
|
def wind_scenario(tmp_path: Path) -> Path:
|
|
data = {
|
|
"wind": {
|
|
"location_id": "RJ",
|
|
"capacity_mw": 10.0,
|
|
}
|
|
}
|
|
p = tmp_path / "scenario.json"
|
|
p.write_text(json.dumps(data))
|
|
return p
|
|
|
|
|
|
def _invoke(scenario: Path, out: Path) -> object:
|
|
# Single-command Typer app: invoke without the subcommand name
|
|
return runner.invoke(app, ["--input", str(scenario), "--output", str(out)])
|
|
|
|
|
|
def test_simulate_gen_solar(solar_scenario: Path, tmp_path: Path) -> None:
|
|
out = tmp_path / "gen.parquet"
|
|
result = _invoke(solar_scenario, out)
|
|
assert result.exit_code == 0, result.output # type: ignore[union-attr]
|
|
assert out.exists()
|
|
assert "Solar Y1 CUF" in result.output # type: ignore[union-attr]
|
|
|
|
|
|
def test_simulate_gen_wind(wind_scenario: Path, tmp_path: Path) -> None:
|
|
out = tmp_path / "gen.parquet"
|
|
result = _invoke(wind_scenario, out)
|
|
assert result.exit_code == 0, result.output # type: ignore[union-attr]
|
|
assert out.exists()
|
|
assert "Wind Y1 PLF" in result.output # type: ignore[union-attr]
|
|
|
|
|
|
def test_simulate_gen_empty_input(tmp_path: Path) -> None:
|
|
p = tmp_path / "empty.json"
|
|
p.write_text("{}")
|
|
out = tmp_path / "gen.parquet"
|
|
result = _invoke(p, out)
|
|
assert result.exit_code != 0 # type: ignore[union-attr]
|
|
|
|
|
|
def test_simulate_gen_parquet_has_rows(solar_scenario: Path, tmp_path: Path) -> None:
|
|
import pandas as pd
|
|
|
|
out = tmp_path / "gen.parquet"
|
|
result = _invoke(solar_scenario, out)
|
|
assert result.exit_code == 0, result.output # type: ignore[union-attr]
|
|
df = pd.read_parquet(out)
|
|
assert len(df) == 25 * 8760
|