From 95863197261d2f3909b447a37ede380078b49b58 Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Wed, 3 Jul 2024 01:00:45 +0000 Subject: [PATCH] add test_utils to consolidate fuel oil & naturalgas tests --- .../tests/test_rules_engine/test_fuel_oil.py | 26 ++++-- .../test_rules_engine/test_natural_gas.py | 75 ++++------------- .../tests/test_rules_engine/test_utils.py | 84 +++++++++++++++++++ 3 files changed, 120 insertions(+), 65 deletions(-) create mode 100644 rules-engine/tests/test_rules_engine/test_utils.py diff --git a/rules-engine/tests/test_rules_engine/test_fuel_oil.py b/rules-engine/tests/test_rules_engine/test_fuel_oil.py index 7c17d2c4..5b0b6472 100644 --- a/rules-engine/tests/test_rules_engine/test_fuel_oil.py +++ b/rules-engine/tests/test_rules_engine/test_fuel_oil.py @@ -1,18 +1,30 @@ """ Tests for fuel-oil related methods. """ +import pathlib +import csv + +from pydantic import BaseModel + +from test_utils import Summary + +from rules_engine.pydantic_models import ( + OilPropaneBillingRecordInput, + OilPropaneBillingInput +) # Test inputs are provided as separate directory within the "cases/examples" directory # Each subdirectory contains a JSON file (named summary.json) which specifies the inputs for the test runner ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" -NATURAL_GAS_DIR = ROOT_DIR / "fuel_oil" - +FUEL_OIL_DIR = ROOT_DIR / "fuel_oil" class Example(BaseModel): summary: Summary - natural_gas_usage: NaturalGasBillingExampleInput + fuel_oil_usage: OilPropaneBillingExampleInput temperature_data: TemperatureInput +class OilPropaneBillingRecordExampleInput(OilPropaneBillingInput): + records: list[OilPropaneBillingRecordInput] @pytest.fixture(scope="module", params=INPUT_DATA) def data(request): @@ -22,19 +34,19 @@ def data(request): """ summary = load_summary(request.param) - if summary.fuel_type == engine.FuelType.GAS: - natural_gas_usage = load_natural_gas( + if summary.fuel_type == engine.FuelType.OIL: + fuel_oil_usage = load_fuel_oil( request.param, summary.estimated_balance_point ) else: - natural_gas_usage = None + fuel_oil_usage = None weather_station_short_name = summary.local_weather_station[:4] temperature_data = load_temperature_data(weather_station_short_name) example = Example( summary=summary, - natural_gas_usage=natural_gas_usage, + fuel_oil_usage=fuel_oil_usage, temperature_data=temperature_data, ) yield example \ No newline at end of file diff --git a/rules-engine/tests/test_rules_engine/test_natural_gas.py b/rules-engine/tests/test_rules_engine/test_natural_gas.py index 6e739211..3747e1a2 100644 --- a/rules-engine/tests/test_rules_engine/test_natural_gas.py +++ b/rules-engine/tests/test_rules_engine/test_natural_gas.py @@ -10,6 +10,8 @@ from pytest import approx from typing_extensions import Annotated +from test_utils import Summary + from rules_engine import engine from rules_engine.pydantic_models import ( NaturalGasBillingInput, @@ -19,6 +21,8 @@ TemperatureInput, ) +from test_utils import load_fuel_billing_example_input + # Test inputs are provided as separate directory within the "cases/examples" directory # Each subdirectory contains a JSON file (named summary.json) which specifies the inputs for the test runner ROOT_DIR = pathlib.Path(__file__).parent / "cases" / "examples" @@ -38,11 +42,21 @@ class Summary(SummaryInput, SummaryOutput): # Extend NG Billing Record Input to capture whole home heat loss input from example data class NaturalGasBillingRecordExampleInput(NaturalGasBillingRecordInput): + """ + whole_home_heat_loss_rate is added to this class solely because of testing needs + and must be included, and this class must be used instead of NaturalGasBillingRecordInput, + which would otherwise intuitively be used, and which is used in production. + """ whole_home_heat_loss_rate: float # Then overload NG Billing Input to contain new NG Billing Record Example Input subclass class NaturalGasBillingExampleInput(NaturalGasBillingInput): + """ + This class exists to contain a list of NaturalGasBillingRecordExampleInput, which + must be used for testing purposes rather than NaturalGasBillingInput, which would + otherwise intuitively used, and which is used in production. + """ records: list[NaturalGasBillingRecordExampleInput] @@ -58,61 +72,6 @@ def load_summary(folder: str) -> Summary: return Summary(**d) -def load_natural_gas( - folder: str, estimated_balance_point: float -) -> NaturalGasBillingExampleInput: - records = [] - - with open(NATURAL_GAS_DIR / folder / "natural-gas.csv") as f: - reader = csv.DictReader(f) - row: Any - for row in reader: - inclusion_override = row["inclusion_override"] - if inclusion_override == "": - inclusion_override = None - else: - inclusion_override = int(inclusion_override) - - # Choose the correct billing period heat loss (aka "ua") column based on the estimated balance point provided in SummaryOutput - ua_column_name = None - # First we will look for an exact match to the value of the estimated balance point - for column_name in row: - if ( - "ua_at_" in column_name - and str(estimated_balance_point) in column_name - ): - ua_column_name = column_name - break - # If we don't find that exact match, we round the balance point up to find our match - # It's possible that with further updates to summary data in xls and regen csv files, we wouldn't have this case - if ua_column_name == None: - ua_column_name = ( - "ua_at_" + str(int(round(estimated_balance_point, 0))) + "f" - ) - ua = ( - row[ua_column_name].replace(",", "").strip() - ) # Remove commas and whitespace to cleanup the data - if bool(ua): - whole_home_heat_loss_rate = float(ua) - else: - whole_home_heat_loss_rate = 0 - - item = NaturalGasBillingRecordExampleInput( - period_start_date=datetime.strptime( - row["start_date"].split(maxsplit=1)[0], "%Y-%m-%d" - ).date(), - period_end_date=datetime.strptime( - row["end_date"].split(maxsplit=1)[0], "%Y-%m-%d" - ).date(), - usage_therms=row["usage"], - inclusion_override=inclusion_override, - whole_home_heat_loss_rate=whole_home_heat_loss_rate, - ) - records.append(item) - - return NaturalGasBillingExampleInput(records=records) - - def load_temperature_data(weather_station: str) -> TemperatureInput: with open(ROOT_DIR / "temperature-data.csv", encoding="utf-8-sig") as f: reader = csv.DictReader(f) @@ -130,14 +89,14 @@ def load_temperature_data(weather_station: str) -> TemperatureInput: @pytest.fixture(scope="module", params=INPUT_DATA) def data(request): """ - Loads the usage and temperature data and summary inputs into an + Loads the usage and temperature data and summary inputs into an Example instance. """ summary = load_summary(request.param) if summary.fuel_type == engine.FuelType.GAS: - natural_gas_usage = load_natural_gas( - request.param, summary.estimated_balance_point + natural_gas_usage = load_fuel_billing_example_input( + request.param, "GAS", summary.estimated_balance_point ) else: natural_gas_usage = None diff --git a/rules-engine/tests/test_rules_engine/test_utils.py b/rules-engine/tests/test_rules_engine/test_utils.py new file mode 100644 index 00000000..5ff601e9 --- /dev/null +++ b/rules-engine/tests/test_rules_engine/test_utils.py @@ -0,0 +1,84 @@ +from pydantic import BaseModel + +import NaturalGasBillingExampleInput +import OilPropaneBillingExampleInput + + +class Summary(SummaryInput, SummaryOutput): + """ + Holds summary.json information alongside a string referring to a + local weather station. + """ + local_weather_station: str + + +def load_fuel_billing_example_input( + folder: str, fuel_type: str, estimated_balance_point: float +) -> NaturalGasBillingExampleInput | OilPropaneBillingExampleInput: + """ + Loads a NaturalGasBillingExampleInput or + OilPropaneBillingExampleInput from an appropriate csv. + + Arguments: + folder - the string path to the file + fuel_type - GAS or OIL, the latter of which refers to propane + too + estimated_balance_point - TODO: Document what this argument is. + """ + records = [] + + with open(folder / fuel_type) as f: + reader = csv.DictReader(f) + row: Any + for row in reader: + inclusion_override = row["inclusion_override"] + if inclusion_override == "": + inclusion_override = None + else: + inclusion_override = int(inclusion_override) + + # Choose the correct billing period heat loss (aka "ua") + # column based on the estimated balance point provided in + # SummaryOutput + ua_column_name = None + # First we will look for an exact match to the value of + # the estimated balance point + for column_name in row: + if ( + "ua_at_" in column_name + and str(estimated_balance_point) in column_name + ): + ua_column_name = column_name + break + # If we don't find that exact match, we round the balance + # point up to find our match. + # It's possible that with further updates to summary data + # in xls and regen csv files, we wouldn't have this case. + if ua_column_name == None: + ua_column_name = ( + "ua_at_" + str(int(round(estimated_balance_point, 0))) + "f" + ) + ua = ( + row[ua_column_name].replace(",", "").strip() + ) # Remove commas and whitespace to cleanup the data + if bool(ua): + whole_home_heat_loss_rate = float(ua) + else: + whole_home_heat_loss_rate = 0 + + item = NaturalGasBillingRecordExampleInput( + period_start_date=datetime.strptime( + row["start_date"].split(maxsplit=1)[0], "%Y-%m-%d" + ).date(), + period_end_date=datetime.strptime( + row["end_date"].split(maxsplit=1)[0], "%Y-%m-%d" + ).date(), + usage_therms=row["usage"], + inclusion_override=inclusion_override, + whole_home_heat_loss_rate=whole_home_heat_loss_rate, + ) + records.append(item) + + return (NaturalGasBillingExampleInput(records=records) + if fuel_type == "GAS" + else OilPropaneBillingExample(records=records))