Skip to content

Commit 9eccd6f

Browse files
committed
Add system test for KPI integration in simulator workflow
- Add test__simulator__kpis_present_in_output asserting KPIs are present in instance[0].area after a full pipeline run, with CAPEX verified against the ESDL costInformation and system_lifetime exercised with a non-default value (25 years) - Add scripts/test_system_local.sh for running system tests with a locally built simulator-worker image via docker-compose.override.local.yml - Gitignore docker-compose.override.local.yml - Update docker-compose.yml to use simulator-worker 0.0.29beta (kpi-calculator integration) - Exclude KPIs from snapshot comparison in existing simulator tests to avoid snapshot drift; KPI correctness is covered by the dedicated test
1 parent fe48e28 commit 9eccd6f

4 files changed

Lines changed: 111 additions & 8 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,5 @@ fabric.properties
219219

220220
unit_test_coverage/
221221
.env.test
222-
gurobi/
222+
gurobi/
223+
docker-compose.override.local.yml

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ services:
215215
- "./gurobi/gurobi.lic:/app/gurobi/gurobi.lic"
216216

217217
omotes_simulator_worker:
218-
image: ghcr.io/project-omotes/omotes-simulator-worker:0.0.28
218+
image: ghcr.io/project-omotes/omotes-simulator-worker:0.0.29beta
219219
restart: unless-stopped
220220
deploy:
221221
replicas: 2

scripts/test_system_local.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
# Run system tests with a locally built simulator-worker image.
3+
# The image is built from ../../simulator-worker-OMOTES/simulator-worker
4+
# via docker-compose.override.local.yml (gitignored).
5+
. scripts/_select_docker_compose.sh
6+
7+
export COMPOSE_PROJECT_NAME=omotes_system_tests
8+
9+
ENV_FILE=".env.test"
10+
DOCKER_COMPOSE_FILE="./docker-compose.yml -f docker-compose.override.local.yml -f system_tests/docker-compose.override.yml"
11+
12+
cp .env.template ${ENV_FILE}
13+
sed -i 's/LOG_LEVEL=[a-z]*/LOG_LEVEL=WARNING/gi' ${ENV_FILE}
14+
15+
$DOCKER_COMPOSE --env-file ${ENV_FILE} -f $DOCKER_COMPOSE_FILE down -v
16+
./scripts/setup.sh $ENV_FILE "./docker-compose.yml -f ./docker-compose.override.setup.yml"
17+
$DOCKER_COMPOSE --env-file ${ENV_FILE} -f $DOCKER_COMPOSE_FILE build
18+
$DOCKER_COMPOSE --env-file ${ENV_FILE} -f $DOCKER_COMPOSE_FILE up --abort-on-container-exit

system_tests/src/test_workflows_steps.py

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
port=int(os.environ.get("RABBITMQ_PORT", "5672")),
3838
)
3939

40+
# KPI content is tested separately in test__simulator__kpis_present_in_output
41+
# and changes with each kpi-calculator release, so exclude it from snapshot comparisons.
42+
# Note: this path assumes xmltodict parses a single <instance> element (dict, not list).
43+
# If multi-instance ESDLs are introduced, this path will need to be updated.
44+
EXCLUDE_KPIS: set[str] = {"root['esdl:EnergySystem']['instance']['area']['KPIs']"}
45+
4046
SQL_CONFIG = {
4147
"host": os.environ.get("POSTGRES_HOST", "localhost"),
4248
"port": int(os.environ.get("POSTGRES_PORT", "6432")),
@@ -205,13 +211,21 @@ def expect_a_result(
205211
f"The job did not finish as {expected_result}. Found {result.result_type}"
206212
)
207213

208-
def compare_esdl(self, expected_esdl: str, result_esdl: str) -> None:
214+
def compare_esdl(
215+
self,
216+
expected_esdl: str,
217+
result_esdl: str,
218+
exclude_paths: set[str] | None = None,
219+
) -> None:
220+
"""Compare two ESDL strings for equality after normalization.
221+
222+
:param exclude_paths: Optional DeepDiff paths to ignore (e.g. EXCLUDE_KPIS).
223+
"""
209224
expected = normalize_esdl(expected_esdl)
210225
result = normalize_esdl(result_esdl)
211-
diff_msg = pformat(DeepDiff(expected, result))
212-
226+
diff = DeepDiff(expected, result, exclude_paths=exclude_paths)
213227
self.assertEqual(
214-
expected, result, msg=f"Found the following differences:\n{diff_msg}"
228+
{}, dict(diff), msg=f"Found the following differences:\n{pformat(diff)}"
215229
)
216230

217231
def test__grow_optimizer_default__happy_path(self) -> None:
@@ -282,7 +296,9 @@ def test__simulator__happy_path(self) -> None:
282296
expected_esdl = retrieve_esdl_file(
283297
"./test_esdl/output/test__simulator__happy_path.esdl"
284298
)
285-
self.compare_esdl(expected_esdl, result_handler.result.output_esdl)
299+
self.compare_esdl(
300+
expected_esdl, result_handler.result.output_esdl, exclude_paths=EXCLUDE_KPIS
301+
)
286302

287303
# assert time series data created
288304
assert_influxdb_database_existence(result_handler.result.output_esdl, True)
@@ -330,7 +346,9 @@ def test__simulator__multiple_ates_run(self) -> None:
330346

331347
for result_handler in result_handlers:
332348
self.expect_a_result(result_handler, JobResult.SUCCEEDED)
333-
self.compare_esdl(expected_esdl, result_handler.result.output_esdl)
349+
self.compare_esdl(
350+
expected_esdl, result_handler.result.output_esdl, exclude_paths=EXCLUDE_KPIS
351+
)
334352

335353
def test__grow_optimizer_default__happy_path_1source(self) -> None:
336354
# Arrange
@@ -702,3 +720,69 @@ def _watch_job(result_handler: OmotesJobHandler):
702720
self.assertTrue(str(high_priority_job.id) in ordered_job_result_ids)
703721
# check that high priority job result was not last (exact order may vary)
704722
self.assertNotEqual(str(high_priority_job.id), ordered_job_result_ids[-1])
723+
724+
def test__simulator__kpis_present_in_output(self) -> None:
725+
"""Test that KPIs are calculated and stored in the output ESDL.
726+
727+
Uses simulator_ates_short_run.esdl which contains costInformation on assets,
728+
allowing the kpi-calculator to produce non-trivial KPI results.
729+
"""
730+
# Arrange
731+
result_handler = OmotesJobHandler()
732+
esdl_file = retrieve_esdl_file(
733+
"./test_esdl/input/simulator_ates_short_run.esdl"
734+
)
735+
workflow_type = "simulator"
736+
timeout_seconds = 60.0
737+
params_dict = {
738+
"timestep": datetime.timedelta(hours=1),
739+
"start_time": datetime.datetime(2019, 1, 1, 0, 0, 0, tzinfo=datetime.UTC),
740+
"end_time": datetime.datetime(2019, 1, 1, 3, 0, 0, tzinfo=datetime.UTC),
741+
"system_lifetime": 25.0,
742+
}
743+
744+
# Act
745+
with omotes_client() as omotes_client_:
746+
submit_a_job(
747+
omotes_client_, esdl_file, workflow_type, params_dict, result_handler
748+
)
749+
result_handler.wait_until_result(timeout_seconds)
750+
751+
# Assert
752+
self.expect_a_result(result_handler, JobResult.SUCCEEDED)
753+
output_esh = esdl.esdl_handler.EnergySystemHandler()
754+
output_esh.load_from_string(result_handler.result.output_esdl)
755+
energy_system = output_esh.energy_system
756+
757+
# KPIs are attached to instance[0].area, not energy_system directly
758+
self.assertGreater(
759+
len(energy_system.instance), 0, "Output ESDL must have at least one instance"
760+
)
761+
main_area = energy_system.instance[0].area
762+
self.assertIsNotNone(main_area, "instance[0] must have an area")
763+
self.assertIsNotNone(main_area.KPIs, "KPIs should be present in the main area")
764+
kpi_list = list(main_area.KPIs.kpi)
765+
self.assertGreater(len(kpi_list), 0, "At least one KPI should be calculated")
766+
for kpi in kpi_list:
767+
self.assertNotEqual(kpi.name, "", "Each KPI should have a name")
768+
769+
# CAPEX: the ATES asset in simulator_ates_short_run.esdl has
770+
# investmentCosts=2333594.0 EUR (bare EUR unit, no conversion factor).
771+
# This is the only asset with cost data so total CAPEX equals that value.
772+
expected_capex = 2_333_594.0
773+
kpi_by_name = {kpi.name: kpi for kpi in kpi_list}
774+
self.assertIn(
775+
"High level cost breakdown [EUR]",
776+
kpi_by_name,
777+
"Cost breakdown KPI missing from output",
778+
)
779+
cost_items = {
780+
item.label: item.value
781+
for item in kpi_by_name["High level cost breakdown [EUR]"].distribution.stringItem
782+
}
783+
self.assertAlmostEqual(
784+
cost_items.get("CAPEX (total)", 0.0),
785+
expected_capex,
786+
places=1,
787+
msg=f"CAPEX should match investmentCosts in simulator_ates_short_run.esdl; got {cost_items}",
788+
)

0 commit comments

Comments
 (0)