from datetime import datetime import pandas as pd import pytest from unittest.mock import Mock from backend.Property import Property from etl.epc_clean.EpcClean import EpcClean from etl.epc.Record import EPCRecord from etl.bill_savings.KwhData import KwhData # Define some test data mock_epc_response = { "rows": [ { "tenure": "rental (social)", "lmk-key": 1, "uprn": 1, "number-habitable-rooms": 5, "property-type": "House", "built-form": "Detached", "inspection-date": "2023-06-01", 'lodgement-datetime': '2023-06-01 20:29:01', 'lodgement-date': '2023-06-01', "some-other-key": "some-value", "roof-description": "pitched, no insulation", "walls-description": "Walls Description", "windows-description": "Fully double glazed", "mainheat-description": "Boiler and radiators, mains gas", "hotwater-description": "From main system", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground", "lighting-cost-current": 123, "heating-cost-current": 800, "hot-water-cost-current": 200 }, { "lmk-key": 2, "uprn": 2, "number-habitable-rooms": 5, "property-type": "House", "built-form": "Detached", "inspection-date": "2023-05-01", 'lodgement-datetime': '2023-05-01 20:29:01', 'lodgement-date': '2023-05-01', "some-other-key": "some-other-value", "roof-description": "Roof Description", "walls-description": "Walls Description", "windows-description": "Fully double glazed", "mainheat-description": "Boiler and radiators, mains gas", "hotwater-description": "From main system", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground", "lighting-cost-current": 123, "heating-cost-current": 800, "hot-water-cost-current": 200 } ] } class TestProperty: @pytest.fixture(autouse=True) def property_instance(self, mock_cleaner): epc_record = EPCRecord() # Set all required attributes directly on epc_record epc_record.uprn = 1 epc_record.lighting_cost_current = 123 epc_record.epc_co2_emissions = 5 epc_record.primary_energy_consumption = 1234 epc_record.roof_description = "pitched, no insulation" epc_record.walls_description = "Walls Description" epc_record.windows_description = "Fully double glazed" epc_record.mainheat_description = "Boiler and radiators, mains gas" epc_record.hotwater_description = "From main system" epc_record.floor_description = "Floor Description" epc_record.floor_level = "Ground" epc_record.property_type = "House" # Add any other attributes needed by the tests property_instance = Property(id=1, postcode="AB12CD", address="Test Address", epc_record=epc_record) property_instance.number_of_floors = 2 property_instance.number_of_rooms = 5 property_instance.floor_area = 100 property_instance.floor_height = 2.5 return property_instance @pytest.fixture() def mock_cleaner(self): lighting_averages = [ {'lighting-description': 'good lighting efficiency', 'low-energy-lighting': 99.26666666666667}, {'lighting-description': 'excellent lighting efficiency', 'low-energy-lighting': 100.0}, {'lighting-description': 'below average lighting efficiency', 'low-energy-lighting': 0.0} ] cleaner_spec = EpcClean( data=[ {"roof-description": "Roof Description"}, {"walls-description": "Walls Description"}, {"windows-description": "Windows Description"}, {"mainheat-description": "Main Heating Description"}, {"hotwater-description": "Hot Water Description"}, {"lighting-description": "Good Lighting Efficiency"}, {"low-energy-lighting": 0}, {"floor-description": "Floor Description"} ], lighting_averages=lighting_averages ) mock_cleaner = Mock(spec=cleaner_spec) walls_data = { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } mock_cleaner.cleaned = { "roof-description": [ {"original_description": "Roof Description"}, {"original_description": "pitched, no insulation", "is_pitched": True, "is_flat": False, "is_roof_room": False} ], "walls-description": [walls_data], "windows-description": [ {'original_description': 'Fully double glazed', 'has_glazing': True, 'glazing_coverage': 'full', 'glazing_type': 'double', 'no_data': False} ], "mainheat-description": [ { 'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': False, 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, "has_electric_heat_pumps": False, "has_micro-cogeneration": False } ], "hotwater-description": [ {'original_description': 'From main system', 'heater_type': None, 'system_type': 'from main system', 'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, 'tariff_type': None, 'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None, 'assumed': False, "appliance": None} ], "lighting-description": [{"original_description": "Good Lighting Efficiency"}], "floor-description": [ {"original_description": "Floor Description", "is_suspended": True, "another_property_below": False}] } return mock_cleaner @pytest.fixture def kwh_client(self): kwh_client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False) # We fix this pricing table for these tests kwh_client.retail_price_comparison = pd.DataFrame( [ { "Date": datetime.today().strftime("%Y-%m-%d"), 'Average standard variable tariff (Large legacy suppliers)': 1 } ] ) kwh_client.retail_price_comparison["Date"] = pd.to_datetime(kwh_client.retail_price_comparison["Date"]) return kwh_client def test_init(self): epc_record = EPCRecord() epc_record.uprn = 1 epc_record.lighting_cost_current = 123 epc_record.epc_co2_emissions = 5 epc_record.primary_energy_consumption = 1234 epc_record.roof_description = "pitched, no insulation" epc_record.walls_description = "Walls Description" epc_record.windows_description = "Fully double glazed" epc_record.mainheat_description = "Boiler and radiators, mains gas" epc_record.hotwater_description = "From main system" epc_record.floor_description = "Floor Description" epc_record.floor_level = "Ground" epc_record.property_type = "House" inst1 = Property(0, postcode="AB12CD", address="Test Address", epc_record=epc_record) assert inst1.epc_record.uprn == 1 inst2 = Property(3, "AB12CD", "Test Address", epc_record=epc_record) assert inst2.id == 3 inst3 = Property(4, "AB12CD", "Test Address", epc_record=epc_record) assert inst3.epc_record.uprn == 1 def test_set_features( self, property_instance, mock_cleaner, kwh_client, ): kwh_predictions = { "heating_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 12000} ] ), "hotwater_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 3000} ] ), } # Ensure required energy and walls attributes are set property_instance.energy["epc_co2_emissions"] = 1.0 property_instance.energy["appliances_co2_emissions"] = 1.0 property_instance.walls = { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } property_instance.set_features( mock_cleaner.cleaned, kwh_client, kwh_predictions ) # ...existing code for assertions... def test_get_components_without_cleaned_data(self, property_instance, mock_cleaner): # Modify the mock EpcClean to not have cleaned data mock_cleaner.cleaned = {} # No direct assignment to prepared_epc here, but for robustness, patch if needed # Verify that ValueError is raised when EpcClean doesn't contain cleaned data with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"): property_instance.set_features(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame()) def test_get_components_no_attributes( self, property_instance, mock_cleaner, kwh_client ): kwh_predictions = { "heating_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 12000} ] ), "hotwater_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 3000} ] ), } # Modify the mock cleaner to have no attributes for a specific description mock_cleaner.cleaned = { "roof-description": [] } property_instance.epc_record.roof_description = "Pitched, no insulation" # Ensure required energy and walls attributes are set property_instance.energy["epc_co2_emissions"] = 1.0 property_instance.energy["appliances_co2_emissions"] = 1.0 property_instance.walls = { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } # Ensure required floor attribute is set property_instance.floor = { "original_description": "Solid, no insulation (assumed)", "clean_description": "Pitched, no insulation", "thermal_transmittance": None, "thermal_transmittance_unit": None, "is_assumed": False, "is_to_unheated_space": False, "is_to_external_air": False, "is_suspended": False, "is_solid": True, "another_property_below": False, "insulation_thickness": "none", "floor_thermal_transmittance": None, "floor_insulation_thickness": "none" } property_instance.set_features( mock_cleaner.cleaned, kwh_client, kwh_predictions ) assert property_instance.roof["clean_description"] == "Pitched, no insulation" assert property_instance.roof["is_pitched"] def test_get_components_multiple_attributes( self, property_instance, mock_cleaner, kwh_client ): # This shouldn't happen - it would mean a cleaning error property_instance.epc_record.roof_description = "Roof Description" cleaned = { "roof-description": [ {"original_description": "Roof Description"}, {"original_description": "Roof Description"} ] } kwh_predictions = { "heating_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 12000} ] ), "hotwater_kwh_predictions": pd.DataFrame( [ {"id": property_instance.epc_record.uprn, "predictions": 3000} ] ), } # Verify that ValueError is raised when multiple attributes are found with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): property_instance.set_features(cleaned, kwh_client, kwh_predictions) def test_set_spatial(self): from unittest.mock import patch, PropertyMock epc_record = EPCRecord() with patch.object(type(epc_record), "prepared_epc", new_callable=PropertyMock) as mock_prepared_epc: mock_prepared_epc.return_value = mock_epc_response["rows"][0] epc_record.uprn = int(mock_epc_response["rows"][0]["uprn"]) prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) spatial1 = pd.DataFrame([{ 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, 'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True }]) prop.set_spatial(spatial1) assert prop.in_conservation_area assert not prop.is_listed assert prop.is_heritage assert prop.restricted_measures prop2 = Property(1, "AB12CD", "Test Address", epc_record=epc_record) spatial2 = pd.DataFrame([{ 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, 'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False }]) prop2.set_spatial(spatial2) assert prop2.in_conservation_area is None assert not prop2.is_listed assert not prop2.is_heritage assert not prop2.restricted_measures def test_set_floor_level(self): # 1st case: floor-level '01', property-type 'Flat' epc_record = EPCRecord() epc_record.floor_level = '01' epc_record.property_type = 'Flat' prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop.floor = { 'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none', } prop.set_floor_level() assert prop.floor_level == 0 # 2nd case: floor-level 'Ground', property-type 'Flat' epc_record = EPCRecord() epc_record.floor_level = 'Ground' epc_record.property_type = 'Flat' prop2 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop2.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop2.set_floor_level() assert prop2.floor_level == 1 # 3rd case: floor-level '02', property-type 'Flat' epc_record = EPCRecord() epc_record.floor_level = '02' epc_record.property_type = 'Flat' prop3 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop3.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop3.set_floor_level() assert prop3.floor_level == 2 # 4th case: floor-level '', property-type 'House' epc_record = EPCRecord() epc_record.floor_level = '' epc_record.property_type = 'House' prop4 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop4.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop4.set_floor_level() assert prop4.floor_level is None