diff --git a/src/libecalc/dto/components.py b/src/libecalc/dto/components.py index a08658bb84..91b5c69cde 100644 --- a/src/libecalc/dto/components.py +++ b/src/libecalc/dto/components.py @@ -17,7 +17,7 @@ from libecalc.common.logger import logger from libecalc.common.priorities import Priorities from libecalc.common.stream_conditions import TimeSeriesStreamConditions -from libecalc.common.string.string_utils import generate_id, get_duplicates +from libecalc.common.string.string_utils import generate_id from libecalc.common.time_utils import Period, Periods from libecalc.common.units import Unit from libecalc.common.utils.rates import ( @@ -690,52 +690,6 @@ def get_component_process_type(self) -> ComponentType: def get_name(self) -> str: return self.name - @model_validator(mode="after") - def validate_unique_names(self): - """Ensure unique component names within installation.""" - names = [self.name] - fuel_types = [FuelType] - fuel_names = [str] - for installation in self.installations: - names.append(installation.name) - fuel_consumers = installation.fuel_consumers - venting_emitters = installation.venting_emitters - - names.extend([venting_emitter.name for venting_emitter in venting_emitters]) - for fuel_consumer in fuel_consumers: - names.append(fuel_consumer.name) - if isinstance(fuel_consumer, GeneratorSet): - for electricity_consumer in fuel_consumer.consumers: - if isinstance(electricity_consumer, ConsumerSystem): - for consumer in electricity_consumer.consumers: - names.append(consumer.name) - elif isinstance(fuel_consumer, ConsumerSystem): - for consumer in fuel_consumer.consumers: - names.append(consumer.name) - if fuel_consumer.fuel is not None: - for fuel_type in fuel_consumer.fuel.values(): - # Need to verify that it is a different fuel - if fuel_type is not None and fuel_type not in fuel_types: - fuel_types.append(fuel_type) - fuel_names.append(fuel_type.name) - - duplicated_names = get_duplicates(names) - duplicated_fuel_names = get_duplicates(fuel_names) - - if len(duplicated_names) > 0: - raise ValueError( - "Component names must be unique. Components include the main model, installations," - " generator sets, electricity consumers, fuel consumers, systems and its consumers and direct emitters." - f" Duplicated names are: {', '.join(duplicated_names)}" - ) - - if len(duplicated_fuel_names) > 0: - raise ValueError( - "Fuel type names must be unique across installations." - f" Duplicated names are: {', '.join(duplicated_fuel_names)}" - ) - return self - def get_graph(self) -> ComponentGraph: graph = ComponentGraph() graph.add_node(self) diff --git a/src/libecalc/presentation/yaml/model.py b/src/libecalc/presentation/yaml/model.py index 89e938f66f..229440f710 100644 --- a/src/libecalc/presentation/yaml/model.py +++ b/src/libecalc/presentation/yaml/model.py @@ -176,6 +176,7 @@ def _get_model_types(yaml_model: YamlValidator) -> dict["ModelName", "ModelConte def _get_validation_context(self, yaml_model: YamlValidator) -> YamlModelValidationContext: return { + YamlModelValidationContextNames.model_name: yaml_model.name, YamlModelValidationContextNames.resource_file_names: [name for name, resource in self.resources.items()], YamlModelValidationContextNames.expression_tokens: self._get_token_references(yaml_model=yaml_model), YamlModelValidationContextNames.model_types: self._get_model_types(yaml_model=yaml_model), diff --git a/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py b/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py index 9f3e6fe30c..521892602e 100644 --- a/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py +++ b/src/libecalc/presentation/yaml/yaml_types/components/yaml_asset.py @@ -1,6 +1,9 @@ -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, model_validator +from pydantic_core.core_schema import ValidationInfo +from libecalc.common.string.string_utils import get_duplicates from libecalc.presentation.yaml.yaml_types import YamlBase +from libecalc.presentation.yaml.yaml_types.components.system.yaml_consumer_system import YamlConsumerSystem from libecalc.presentation.yaml.yaml_types.components.yaml_installation import ( YamlInstallation, ) @@ -16,6 +19,7 @@ YamlDefaultDatetime, ) from libecalc.presentation.yaml.yaml_types.yaml_variable import YamlVariables +from libecalc.presentation.yaml.yaml_validation_context import YamlModelValidationContextNames class YamlAsset(YamlBase): @@ -71,3 +75,59 @@ class YamlAsset(YamlBase): title="END", description="Global end date for eCalc calculations in format." "\n\n$ECALC_DOCS_KEYWORDS_URL/END", ) + + @model_validator(mode="after") + def validate_unique_component_names(self, info: ValidationInfo): + """Ensure unique component names in model.""" + context = info.context + if not context: + return self + + if not context.get(YamlModelValidationContextNames.model_name): + return self + + names = [context.get(YamlModelValidationContextNames.model_name)] + + for installation in self.installations: + names.append(installation.name) + for fuel_consumer in installation.fuel_consumers or []: + names.append(fuel_consumer.name) + if isinstance(fuel_consumer, YamlConsumerSystem): + for consumer in fuel_consumer.consumers: + names.append(consumer.name) + + for generator_set in installation.generator_sets or []: + names.append(generator_set.name) + for electricity_consumer in generator_set.consumers: + names.append(electricity_consumer.name) + if isinstance(electricity_consumer, YamlConsumerSystem): + for consumer in electricity_consumer.consumers: + names.append(consumer.name) + + for venting_emitter in installation.venting_emitters or []: + names.append(venting_emitter.name) + + duplicated_names = get_duplicates(names) + + if len(duplicated_names) > 0: + raise ValueError( + "Component names must be unique. Components include the main model, installations," + " generator sets, electricity consumers, fuel consumers, systems and its consumers and direct emitters." + f" Duplicated names are: {', '.join(duplicated_names)}" + ) + + return self + + @model_validator(mode="after") + def validate_unique_fuel_names(self): + fuel_names = [] + + for fuel_type in self.fuel_types: + fuel_names.append(fuel_type.name) + + duplicated_fuel_names = get_duplicates(fuel_names) + if len(duplicated_fuel_names) > 0: + raise ValueError( + "Fuel type names must be unique." f" Duplicated names are: {', '.join(duplicated_fuel_names)}" + ) + return self diff --git a/src/libecalc/presentation/yaml/yaml_validation_context.py b/src/libecalc/presentation/yaml/yaml_validation_context.py index 6b4f4b3900..1513d88554 100644 --- a/src/libecalc/presentation/yaml/yaml_validation_context.py +++ b/src/libecalc/presentation/yaml/yaml_validation_context.py @@ -18,6 +18,7 @@ class YamlModelValidationContextNames: resource_file_names = "resource_file_names" expression_tokens = "expression_tokens" model_types = "model_types" + model_name = "model_name" YamlModelValidationContext = TypedDict( @@ -26,6 +27,7 @@ class YamlModelValidationContextNames: YamlModelValidationContextNames.resource_file_names: list[str], # type: ignore YamlModelValidationContextNames.expression_tokens: list[str], YamlModelValidationContextNames.model_types: dict[ModelName, ModelContext], + YamlModelValidationContextNames.model_name: str, }, total=True, ) diff --git a/src/libecalc/testing/facility_resource_factories.py b/src/libecalc/testing/facility_resource_factories.py new file mode 100644 index 0000000000..da7160db78 --- /dev/null +++ b/src/libecalc/testing/facility_resource_factories.py @@ -0,0 +1,11 @@ +from libecalc.presentation.yaml.yaml_entities import MemoryResource + + +def el2fuel_factory(electricity: list[float] = None, fuel: list[float] = None) -> MemoryResource: + if electricity is None: + electricity = [1, 2, 3, 4, 5] + + if fuel is None: + fuel = [el * 2 for el in electricity] + + return MemoryResource(headers=["POWER", "FUEL"], data=[electricity, fuel]) diff --git a/src/libecalc/testing/yaml_builder.py b/src/libecalc/testing/yaml_builder.py index 42a61be647..fca7bf8ec5 100644 --- a/src/libecalc/testing/yaml_builder.py +++ b/src/libecalc/testing/yaml_builder.py @@ -1,6 +1,6 @@ import abc from enum import Enum -from typing import List, Self, TypeVar, Generic, get_args, cast, Union, Literal +from typing import Generic, List, Literal, Self, TypeVar, Union, get_args from typing_extensions import get_original_bases @@ -10,12 +10,11 @@ FuelTypeUserDefinedCategoryType, InstallationUserDefinedCategoryType, ) - from libecalc.presentation.yaml.yaml_types import YamlBase from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model import ( - YamlFuelEnergyUsageModel, YamlElectricityEnergyUsageModel, YamlEnergyUsageModelCompressor, + YamlFuelEnergyUsageModel, ) from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model.yaml_energy_usage_model_direct import ( ConsumptionRateType, @@ -29,37 +28,37 @@ from libecalc.presentation.yaml.yaml_types.components.yaml_generator_set import YamlGeneratorSet from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation from libecalc.presentation.yaml.yaml_types.emitters.yaml_venting_emitter import ( + YamlDirectTypeEmitter, + YamlOilTypeEmitter, + YamlVentingEmission, YamlVentingEmitter, + YamlVentingType, YamlVentingVolume, YamlVentingVolumeEmission, - YamlVentingType, - YamlOilTypeEmitter, - YamlVentingEmission, - YamlDirectTypeEmitter, ) from libecalc.presentation.yaml.yaml_types.facility_model.yaml_facility_model import ( + YamlCompressorTabularModel, + YamlFacilityAdjustment, YamlFacilityModel, - YamlGeneratorSetModel, YamlFacilityModelType, - YamlFacilityAdjustment, - YamlCompressorTabularModel, + YamlGeneratorSetModel, ) from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_emission import YamlEmission from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType from libecalc.presentation.yaml.yaml_types.models import YamlConsumerModel from libecalc.presentation.yaml.yaml_types.models.model_reference_validation import ( - GeneratorSetModelReference, CompressorEnergyUsageModelModelReference, + GeneratorSetModelReference, ) from libecalc.presentation.yaml.yaml_types.time_series.yaml_time_series import ( - YamlTimeSeriesCollection, YamlDefaultTimeSeriesCollection, + YamlTimeSeriesCollection, ) from libecalc.presentation.yaml.yaml_types.yaml_stream_conditions import ( - YamlOilRateUnits, - YamlOilVolumeRate, YamlEmissionRate, YamlEmissionRateUnits, + YamlOilRateUnits, + YamlOilVolumeRate, ) from libecalc.presentation.yaml.yaml_types.yaml_temporal_model import YamlTemporalModel from libecalc.presentation.yaml.yaml_types.yaml_variable import YamlVariables @@ -315,7 +314,9 @@ def __init__(self): def with_test_data(self) -> Self: self.name = "base load" self.category = ConsumerUserDefinedCategoryType.FIXED_PRODUCTION_LOAD.value - self.energy_usage_model = YamlEnergyUsageModelDirectBuilder().with_test_data().validate() + self.energy_usage_model = ( + YamlEnergyUsageModelDirectBuilder().with_test_data().with_load("0.5").with_fuel_rate(None).validate() + ) return self def with_name(self, name: str) -> Self: @@ -378,7 +379,7 @@ def with_test_data(self) -> Self: self.category = ConsumerUserDefinedCategoryType.TURBINE_GENERATOR self.fuel = YamlFuelTypeBuilder().with_test_data().validate().name self.electricity2fuel = YamlElectricity2fuelBuilder().with_test_data().validate().name - self.consumers.append(YamlElectricityConsumerBuilder().with_test_data().validate()) + self.consumers = [YamlElectricityConsumerBuilder().with_test_data().validate()] return self @@ -539,7 +540,7 @@ def with_test_data(self): self.name = "DefaultInstallation" self.hydrocarbon_export = 0 self.regularity = 1 - self.fuel_consumers.append(YamlFuelConsumerBuilder().with_test_data().validate()) + self.fuel_consumers = [YamlFuelConsumerBuilder().with_test_data().validate()] return self def with_name(self, name: str) -> Self: diff --git a/tests/conftest.py b/tests/conftest.py index eca989636d..4959b1bb58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Optional, cast - import pytest import yaml @@ -28,6 +27,7 @@ YamlEnergyUsageModelDirectBuilder, YamlFuelConsumerBuilder, YamlFuelTypeBuilder, + YamlGeneratorSetBuilder, YamlInstallationBuilder, ) @@ -200,6 +200,11 @@ def yaml_fuel_type_builder_factory(): return lambda: YamlFuelTypeBuilder() +@pytest.fixture +def yaml_generator_set_builder_factory(): + return lambda: YamlGeneratorSetBuilder() + + @pytest.fixture def minimal_installation_yaml_factory(yaml_installation_builder_factory): def minimal_installation_yaml( @@ -247,8 +252,9 @@ def minimal_model_yaml(fuel_rate: int | str = 50) -> ConfigurationService: .with_installations([installation]) .with_start("2020-01-01") .with_end("2023-01-01") + .validate() ) - return yaml_asset_configuration_service_factory(model.validate(), name="minimal_model") + return yaml_asset_configuration_service_factory(model, name="minimal_model") return minimal_model_yaml diff --git a/tests/ecalc_cli/test_app.py b/tests/ecalc_cli/test_app.py index 9b582828ee..f481fbb900 100644 --- a/tests/ecalc_cli/test_app.py +++ b/tests/ecalc_cli/test_app.py @@ -459,31 +459,6 @@ def test_yaml_file_error(self): f"Allowed characters are {COMPONENT_NAME_ALLOWED_CHARS}" in str(ee.value) ) - def test_yaml_duplicate_fuel(self, simple_duplicate_names_yaml_path, tmp_path): - """ - TEST SCOPE: Check that duplicate fuel type names are not allowed in Yaml file. - - Args: - simple model file with duplicate fuel names: - - Returns: - - """ - with pytest.raises(EcalcError) as exc_info: - runner.invoke( - main.app, - _get_args( - model_file=simple_duplicate_names_yaml_path, - csv=True, - output_folder=tmp_path, - name_prefix="test", - output_frequency="YEAR", - ), - catch_exceptions=False, - ) - - assert "Duplicated names are: fuel_gas" in str(exc_info.value) - def test_yaml_duplicate_emissions_in_fuel(self, simple_duplicate_emissions_yaml_path, tmp_path): """ TEST SCOPE: Check that duplicate emission names for one fuel type are not allowed in Yaml file. diff --git a/tests/libecalc/dto/test_fuel_consumer.py b/tests/libecalc/dto/test_fuel_consumer.py index fed16853b5..a1247fec44 100644 --- a/tests/libecalc/dto/test_fuel_consumer.py +++ b/tests/libecalc/dto/test_fuel_consumer.py @@ -107,79 +107,3 @@ def test_missing_fuel(self): user_defined_category="category", ) assert "Missing fuel for fuel consumer 'test'" in str(exc_info.value) - - def test_duplicate_fuel_names(self): - """ - TEST SCOPE: Check that duplicate fuel type names are not allowed. - - Duplicate fuel type names should not be allowed across installations. - Duplicate names may lead to debug problems and overriding of previous - values without user noticing. This test checks that different fuels cannot - have same name. - """ - fuel_consumer1 = get_fuel_consumer( - consumer_name="flare", - fuel_type=get_fuel("fuel1", emission_name="co2"), - category={Period(datetime(2000, 1, 1)): libecalc.dto.types.ConsumerUserDefinedCategoryType.FLARE}, - ) - - fuel_consumer2 = get_fuel_consumer( - consumer_name="boiler", - fuel_type=get_fuel("fuel1", emission_name="ch4"), - category={Period(datetime(2000, 1, 1)): libecalc.dto.types.ConsumerUserDefinedCategoryType.BOILER}, - ) - - installation1 = get_installation("INST1", fuel_consumer1) - installation2 = get_installation("INST2", fuel_consumer2) - - with pytest.raises(ValidationError) as exc_info: - dto.Asset( - name="multiple_installations_asset", - installations=[ - installation1, - installation2, - ], - ) - - assert "Duplicated names are: fuel1" in str(exc_info.value) - - def test_same_fuel(self): - """ - TEST SCOPE: Check that validation of duplicate fuel type names do not reject - when same fuel is used across installations. - - Even though duplicate fuel type names are not allowed across installations, - it should be possible to reuse the same fuel. This test verifies that this still - works. - """ - - fuel_consumer1 = get_fuel_consumer( - consumer_name="flare", - fuel_type=get_fuel("fuel1", emission_name="co2"), - category={Period(datetime(2000, 1, 1)): libecalc.dto.types.ConsumerUserDefinedCategoryType.FLARE}, - ) - - fuel_consumer2 = get_fuel_consumer( - consumer_name="boiler", - fuel_type=get_fuel("fuel1", emission_name="co2"), - category={Period(datetime(2000, 1, 1)): libecalc.dto.types.ConsumerUserDefinedCategoryType.BOILER}, - ) - - installation1 = get_installation("INST1", fuel_consumer1) - installation2 = get_installation("INST2", fuel_consumer2) - - asset = dto.Asset( - name="multiple_installations_asset", - installations=[ - installation1, - installation2, - ], - ) - fuel_types = [] - for inst in asset.installations: - for fuel_consumer in inst.fuel_consumers: - for fuel_type in fuel_consumer.fuel.values(): - if fuel_type not in fuel_types: - fuel_types.append(fuel_type) - - assert len(fuel_types) == 1 diff --git a/tests/libecalc/presentation/yaml/test_yaml_model_unique_name.py b/tests/libecalc/presentation/yaml/test_yaml_model_unique_name.py new file mode 100644 index 0000000000..83d67c66eb --- /dev/null +++ b/tests/libecalc/presentation/yaml/test_yaml_model_unique_name.py @@ -0,0 +1,236 @@ +import itertools +from dataclasses import dataclass + +import pytest +from inline_snapshot import snapshot + +from libecalc.common.time_utils import Frequency +from libecalc.presentation.yaml.model import YamlModel +from libecalc.presentation.yaml.model_validation_exception import ModelValidationException +from libecalc.testing.facility_resource_factories import el2fuel_factory +from libecalc.testing.yaml_builder import ( + YamlAssetBuilder, + YamlElectricity2fuelBuilder, + YamlElectricityConsumerBuilder, + YamlFuelConsumerBuilder, + YamlFuelTypeBuilder, + YamlGeneratorSetBuilder, + YamlInstallationBuilder, + YamlVentingEmitterOilTypeBuilder, +) + + +@pytest.fixture +def duplicate_genset_name_model( + yaml_asset_configuration_service_factory, + resource_service_factory, +): + el2fuel_reference = "el2fuel" + el2fuel_resource_reference = "el2fuel_file" + model = ( + YamlAssetBuilder() + .with_test_data() + .with_facility_inputs( + [ + YamlElectricity2fuelBuilder() + .with_test_data() + .with_name(el2fuel_reference) + .with_file(el2fuel_resource_reference) + .validate() + ] + ) + .with_installations( + [ + YamlInstallationBuilder() + .with_test_data() + .with_generator_sets( + [ + YamlGeneratorSetBuilder() + .with_test_data() + .with_name("genset1") + .with_electricity2fuel(el2fuel_reference) + .with_consumers([YamlElectricityConsumerBuilder().with_test_data().with_name("el1").validate()]) + .construct(), + YamlGeneratorSetBuilder() + .with_test_data() + .with_name("genset1") + .with_electricity2fuel(el2fuel_reference) + .with_consumers([YamlElectricityConsumerBuilder().with_test_data().with_name("el2").validate()]) + .construct(), + ] + ) + .with_fuel_consumers([YamlFuelConsumerBuilder().with_test_data().validate()]) + .construct(), + ] + ) + .construct() + ) + configuration_service = yaml_asset_configuration_service_factory(model, name="invalid_model") + + return YamlModel( + configuration_service=configuration_service, + resource_service=resource_service_factory({el2fuel_resource_reference: el2fuel_factory()}), + output_frequency=Frequency.NONE, + ) + + +@pytest.mark.snapshot +@pytest.mark.inlinesnapshot +def test_genset_duplicate_names(duplicate_genset_name_model): + with pytest.raises(ModelValidationException) as exc_info: + duplicate_genset_name_model.validate_for_run() + + errors = exc_info.value.errors() + assert len(errors) == 1 + assert errors[0].message == snapshot( + "Value error, Component names must be unique. Components include the main model, installations, generator sets, electricity consumers, fuel consumers, systems and its consumers and direct emitters. Duplicated names are: genset1" + ) + + +@dataclass +class DuplicateComponents: + genset: bool + electricity_consumer: bool + fuel_consumer: bool + venting_emitter: bool + installation: bool + asset: bool + + +def generate_model( + duplicate_components: DuplicateComponents, + yaml_asset_configuration_service_factory, + resource_service_factory, + duplicate_name: str, +) -> YamlModel: + def get_name(component_type: str): + should_duplicate = getattr(duplicate_components, component_type) + return duplicate_name if should_duplicate else f"{component_type}1" + + el2fuel_reference = "el2fuel" + el2fuel_resource_reference = "el2fuel_file" + model = ( + YamlAssetBuilder() + .with_test_data() + .with_facility_inputs( + [ + YamlElectricity2fuelBuilder() + .with_test_data() + .with_name(el2fuel_reference) + .with_file(el2fuel_resource_reference) + .validate() + ] + ) + .with_installations( + [ + YamlInstallationBuilder() + .with_test_data() + .with_name(get_name("installation")) + .with_generator_sets( + [ + YamlGeneratorSetBuilder() + .with_test_data() + .with_name(get_name("genset")) + .with_electricity2fuel(el2fuel_reference) + .with_consumers( + [ + YamlElectricityConsumerBuilder() + .with_test_data() + .with_name(get_name("electricity_consumer")) + .validate() + ] + ) + .validate(), + ] + ) + .with_fuel_consumers( + [YamlFuelConsumerBuilder().with_test_data().with_name(get_name("fuel_consumer")).validate()] + ) + .with_venting_emitters( + [ + YamlVentingEmitterOilTypeBuilder() + .with_test_data() + .with_name(get_name("venting_emitter")) + .validate() + ] + ) + .construct(), + ] + ) + .construct() + ) + configuration_service = yaml_asset_configuration_service_factory(model, name=get_name("asset")) + + return YamlModel( + configuration_service=configuration_service, + resource_service=resource_service_factory({el2fuel_resource_reference: el2fuel_factory()}), + output_frequency=Frequency.NONE, + ) + + +@pytest.mark.snapshot +@pytest.mark.inlinesnapshot +@pytest.mark.parametrize( + "first_duplicate, second_duplicate", + itertools.combinations( + ("genset", "electricity_consumer", "fuel_consumer", "venting_emitter", "installation", "asset"), 2 + ), +) +def test_duplicate_names_combinations( + first_duplicate, second_duplicate, yaml_asset_configuration_service_factory, resource_service_factory +): + duplicate_names = DuplicateComponents( + genset="genset" in [first_duplicate, second_duplicate], + electricity_consumer="electricity_consumer" in [first_duplicate, second_duplicate], + fuel_consumer="fuel_consumer" in [first_duplicate, second_duplicate], + venting_emitter="venting_emitter" in [first_duplicate, second_duplicate], + installation="installation" in [first_duplicate, second_duplicate], + asset="asset" in [first_duplicate, second_duplicate], + ) + model = generate_model( + duplicate_names, + yaml_asset_configuration_service_factory, + resource_service_factory, + duplicate_name="duplicationedness", + ) + with pytest.raises(ModelValidationException) as exc_info: + model.validate_for_run() + + errors = exc_info.value.errors() + assert len(errors) == 1 + assert errors[0].message == snapshot( + "Value error, Component names must be unique. Components include the main model, installations, generator sets, electricity consumers, fuel consumers, systems and its consumers and direct emitters. Duplicated names are: duplicationedness" + ) + + +def test_fuel_types_unique_name(yaml_asset_configuration_service_factory, resource_service_factory): + """ + TEST SCOPE: Check that duplicate fuel type names are not allowed. + + Duplicate fuel type names should not be allowed across installations. + Duplicate names may lead to debug problems and overriding of previous + values without user noticing. This test checks that different fuels cannot + have same name. + """ + model = ( + YamlAssetBuilder() + .with_test_data() + .with_fuel_types( + [ + YamlFuelTypeBuilder().with_test_data().with_name("same").construct(), + YamlFuelTypeBuilder().with_test_data().with_name("same").construct(), + ] + ) + .construct() + ) + yaml_model = YamlModel( + configuration_service=yaml_asset_configuration_service_factory(model, "non_unique_fuel_names"), + resource_service=resource_service_factory({}), + output_frequency=Frequency.NONE, + ) + with pytest.raises(ModelValidationException) as exc_info: + yaml_model.validate_for_run() + + errors = exc_info.value.errors() + assert len(errors) == 1 + assert errors[0].message == snapshot("Value error, Fuel type names must be unique. Duplicated names are: same")