diff --git a/changelog.md b/changelog.md index 6139b2ab..8ee7d087 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ ### Fixes * Moved ZepbenTokenAuth to use python dataclasses instead of `zepben.ewb.dataclassy`, existing code should work as is. * `TypeError`s occurring in `StepAction`s will no longer silently pass +* Drop python 3.9 from list of test envs in tox ### Notes * None. diff --git a/pyproject.toml b/pyproject.toml index 01fc985c..05ca3b53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ "pytest-asyncio==1.1.0", "pytest-timeout==2.4.0", "pytest-subtests==0.14.2", - "hypothesis==6.138.2", + "hypothesis==6.140.3", "grpcio-testing==1.61.3", "pylint==2.14.5", "six==1.16.0", diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 18e30342..943eaebf 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -83,6 +83,8 @@ def f(bitmask: int, nxt: Appliance) -> int: if len(appliances) > 1: _ = [acc := f(acc, app) for app in appliances[1:]] self._bitmask = acc + else: + raise TypeError('appliances must be a int or Appliance') @property def bitmask(self): diff --git a/src/zepben/ewb/services/common/difference.py b/src/zepben/ewb/services/common/difference.py index cba02ad4..0e0e865a 100644 --- a/src/zepben/ewb/services/common/difference.py +++ b/src/zepben/ewb/services/common/difference.py @@ -30,8 +30,8 @@ class CollectionDifference(Difference): @dataclass() class ObjectDifference(Difference): - source: T - target: T + source: IdentifiedObject + target: IdentifiedObject differences: Dict[str, Difference] = field(default_factory=dict) diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index 559551b7..e17ca143 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -818,7 +818,7 @@ def create_equipment(include_runtime: bool): def create_equipment_container(include_runtime: bool, add_equipment: bool = True): equipment = { - "equipment": lists(sampled_equipment(include_runtime), min_size=1, max_size=2) + "equipment": lists(sampled_equipment(include_runtime), min_size=1, max_size=30) } if add_equipment else {} return { diff --git a/test/cim/extensions/iec61968/metering/test_pan_demand_response_function.py b/test/cim/extensions/iec61968/metering/test_pan_demand_response_function.py index 91cb58c6..f15ac8f9 100644 --- a/test/cim/extensions/iec61968/metering/test_pan_demand_response_function.py +++ b/test/cim/extensions/iec61968/metering/test_pan_demand_response_function.py @@ -4,10 +4,10 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from pytest import raises from hypothesis import given -from hypothesis.strategies import sampled_from, integers +from hypothesis.strategies import sampled_from, lists, builds -from cim.iec61968.metering.test_end_device_function import end_device_function_kwargs, end_device_function_args, verify_end_device_function_constructor_default, \ - verify_end_device_function_constructor_args +from cim.iec61968.metering.test_end_device_function import end_device_function_kwargs, end_device_function_args, \ + verify_end_device_function_constructor_default, verify_end_device_function_constructor_args from test.cim.iec61968.metering.test_end_device_function import verify_end_device_function_constructor_kwargs from zepben.ewb import PanDemandResponseFunction, ControlledAppliance, Appliance from zepben.ewb.model.cim.iec61968.metering.end_device_function_kind import EndDeviceFunctionKind @@ -15,10 +15,10 @@ pan_demand_response_function_kwargs = { **end_device_function_kwargs, "kind": sampled_from(EndDeviceFunctionKind), - "appliance": integers(min_value=0, max_value=4095) + "appliance": builds(ControlledAppliance, appliances=lists(sampled_from(Appliance), max_size=4, min_size=1, unique=True)) } -pan_demand_response_function_args = [*end_device_function_args, EndDeviceFunctionKind.demandResponse, 1] +pan_demand_response_function_args = [*end_device_function_args, EndDeviceFunctionKind.demandResponse, Appliance.IRRIGATION_PUMP] def test_pan_demand_response_function_constructor_default(): @@ -36,7 +36,7 @@ def test_pan_demand_response_function_constructor_kwargs(kind, appliance, **kwar verify_end_device_function_constructor_kwargs(pdrf, **kwargs) assert pdrf.kind == kind - assert pdrf.appliance.bitmask == appliance + assert pdrf.appliance == appliance def test_pan_demand_response_function_constructor_args(): @@ -44,7 +44,8 @@ def test_pan_demand_response_function_constructor_args(): verify_end_device_function_constructor_args(pdrf) - assert pan_demand_response_function_args[-2:] == [pdrf.kind, pdrf.appliance.bitmask] + assert pan_demand_response_function_args[-2] == pdrf.kind + assert pan_demand_response_function_args[-1].bitmask == pdrf.appliance.bitmask def test_constructor_with_controlled_appliance(): diff --git a/test/services/common/translator/base_test_translator.py b/test/services/common/translator/base_test_translator.py index 019e841f..ebfa2b97 100644 --- a/test/services/common/translator/base_test_translator.py +++ b/test/services/common/translator/base_test_translator.py @@ -1,12 +1,15 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from traceback import print_tb, format_tb -from typing import TypeVar, Type, Set +from traceback import print_tb +from typing import TypeVar, Type, Set, Any + +from hypothesis import given +from hypothesis.strategies import SearchStrategy from zepben.ewb import IdentifiedObject, BaseService, BaseServiceComparator, EquipmentContainer, OperationalRestriction, ConnectivityNode, TableVersion, \ - TableMetadataDataSources, TableNameTypes, TableNames, SqliteTable + TableMetadataDataSources, TableNameTypes, TableNames, SqliteTable, NetworkService, CustomerService, DiagramService from zepben.ewb.database.sqlite.common.base_database_tables import BaseDatabaseTables from zepben.protobuf.cim.iec61970.base.core.IdentifiedObject_pb2 import IdentifiedObject as PBIdentifiedObject @@ -16,15 +19,15 @@ def validate_service_translations( - service_type: Type[BaseService], + service_type: Type[NetworkService | CustomerService | DiagramService], comparator: BaseServiceComparator, database_tables: BaseDatabaseTables, excluded_tables: Set[Type[SqliteTable]], - **kwargs + types_to_test: dict[str, SearchStrategy[Any]], ): expected_tables = {it.__class__ for it in database_tables.tables} - excluded_tables - _excluded_base_tables - if len(kwargs) != len(expected_tables): - actual = {k.removeprefix("create_") for k, v in kwargs.items()} + if len(types_to_test) != len(expected_tables): + actual = {k.removeprefix("create_") for k in types_to_test.keys()} expected = {it().name for it in expected_tables} # create variant without the last two letters to cater for `s` and `es` plurals in the logging. @@ -47,37 +50,45 @@ def validate_service_translations( print() diffs = {} processing = "" + try: - for desc, cim in kwargs.items(): - processing = f"blank {desc}" - blank = type(cim)() - - # Convert the blank object to protobuf and ensure it didn't get converted to an instance of PBIdentifiedObject, - # which indicates a missing `to_pb` implementation or import. - blank_as_pb = blank.to_pb() - assert type(blank_as_pb) is not PBIdentifiedObject, f"There is something wrong with {type(cim)}.to_pb. It is calling directly to the base class." - - # noinspection PyUnresolvedReferences - translated_blank = service_type().add_from_pb(blank_as_pb) - assert translated_blank is not None, f"{blank_as_pb}: Failed to add the translated protobuf object to the service." - assert type(translated_blank) is type(cim), f"{translated_blank}: Converted object should be the same type as {cim}" - - result = comparator.compare_objects(blank, translated_blank) - if result.differences: - diffs[f"blank {desc}"] = result - - processing = f"populated {desc}" - _remove_unsent_references(cim) - # noinspection PyUnresolvedReferences - service = service_type() # outside _add_with_unresolved_references so weak references on `cim` cant be garbage collected before being compared. - result = comparator.compare_objects(cim, _add_with_unresolved_references(service, cim)) - if result.differences: - diffs[f"populated {desc}"] = result + for desc, cim_builder in types_to_test.items(): + print(desc) + + @given(cim_builder) + def run_test(cim): + nonlocal processing + processing = f"blank {desc}" + blank = type(cim)() + + # Convert the blank object to protobuf and ensure it didn't get converted to an instance of PBIdentifiedObject, + # which indicates a missing `to_pb` implementation or import. + blank_as_pb = blank.to_pb() + assert type( + blank_as_pb) is not PBIdentifiedObject, f"There is something wrong with {type(cim)}.to_pb. It is calling directly to the base class." + + # noinspection PyUnresolvedReferences + translated_blank = service_type().add_from_pb(blank_as_pb) + assert translated_blank is not None, f"{blank_as_pb}: Failed to add the translated protobuf object to the service." + assert type(translated_blank) is type(cim), f"{translated_blank}: Converted object should be the same type as {cim}" + + result = comparator.compare_objects(blank, translated_blank) + if result.differences: + diffs[f"blank {desc}"] = result + + processing = f"populated {desc}" + _remove_unsent_references(cim) + # outside _add_with_unresolved_references so weak references on `cim` cant be garbage collected before being compared. + service = service_type() + result = comparator.compare_objects(cim, _add_with_unresolved_references(service, cim)) + if result.differences: + diffs[f"populated {desc}"] = result + + run_test() except BaseException as e: print("###########################") print(f"Processing {processing}:") print(f"Exception [{type(e).__name__}: {e}") - test = format_tb(e.__traceback__) print_tb(e.__traceback__) print("---------------------------") print(diffs) @@ -88,7 +99,6 @@ def validate_service_translations( print(diffs) assert not diffs - def _format_validation_error(description: str, classes: Set[str]) -> str: return f"\n{description}: {classes}\n" if classes else "" diff --git a/test/services/customer/translator/test_customer_translator.py b/test/services/customer/translator/test_customer_translator.py index 05d7ac5f..08d25d27 100644 --- a/test/services/customer/translator/test_customer_translator.py +++ b/test/services/customer/translator/test_customer_translator.py @@ -4,7 +4,6 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import TypeVar -from hypothesis import given from zepben.ewb import IdentifiedObject, CustomerService, NameType, CustomerDatabaseTables, TableCustomerAgreementsPricingStructures, \ TablePricingStructuresTariffs from zepben.ewb.services.common.translator.base_proto2cim import get_nullable @@ -35,8 +34,7 @@ } -@given(**types_to_test) -def test_customer_service_translations(**kwargs): +def test_customer_service_translations(): validate_service_translations( CustomerService, CustomerServiceComparator(), @@ -45,7 +43,7 @@ def test_customer_service_translations(**kwargs): TableCustomerAgreementsPricingStructures, TablePricingStructuresTariffs }, - **kwargs + types_to_test=types_to_test, ) diff --git a/test/services/diagram/translator/test_diagram_translator.py b/test/services/diagram/translator/test_diagram_translator.py index 242cead2..56820126 100644 --- a/test/services/diagram/translator/test_diagram_translator.py +++ b/test/services/diagram/translator/test_diagram_translator.py @@ -4,7 +4,6 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import TypeVar -from hypothesis import given from zepben.ewb import IdentifiedObject, DiagramService, NameType, DiagramDatabaseTables, TableDiagramObjectPoints from zepben.ewb.services.common.translator.base_proto2cim import get_nullable from zepben.ewb.services.diagram.diagram_service_comparator import DiagramServiceComparator @@ -26,8 +25,7 @@ } -@given(**types_to_test) -def test_diagram_service_translations(**kwargs): +def test_diagram_service_translations(): validate_service_translations( DiagramService, DiagramServiceComparator(), @@ -35,7 +33,7 @@ def test_diagram_service_translations(**kwargs): excluded_tables={ TableDiagramObjectPoints }, - **kwargs + types_to_test=types_to_test, ) diff --git a/test/services/network/translator/test_network_translator.py b/test/services/network/translator/test_network_translator.py index 5a185c33..a160d5b3 100644 --- a/test/services/network/translator/test_network_translator.py +++ b/test/services/network/translator/test_network_translator.py @@ -1,17 +1,15 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import TypeVar import pytest -from hypothesis import given, HealthCheck, settings -from database.sqlite.schema_utils import assume_non_blank_street_address_details from services.common.translator.base_test_translator import validate_service_translations from test.cim.cim_creators import * -from zepben.ewb import IdentifiedObject, PowerTransformerEnd, PowerTransformer, NetworkService, Location, NetworkServiceComparator, NameType, \ - NetworkDatabaseTables, TableLocations, TableAssetOrganisationRolesAssets, TableCircuitsSubstations, TableCircuitsTerminals, \ +from zepben.ewb import IdentifiedObject, PowerTransformerEnd, PowerTransformer, NetworkService, NetworkServiceComparator, NameType, \ + NetworkDatabaseTables, TableAssetOrganisationRolesAssets, TableCircuitsSubstations, TableCircuitsTerminals, \ TableEquipmentEquipmentContainers, TableEquipmentOperationalRestrictions, TableEquipmentUsagePoints, TableLoopsSubstations, \ TableProtectionRelayFunctionsProtectedSwitches, TableProtectionRelaySchemesProtectionRelayFunctions, TableUsagePointsEndDevices, \ TableLocationStreetAddresses, TablePositionPoints, TablePowerTransformerEndRatings, TableProtectionRelayFunctionThresholds, \ @@ -94,8 +92,7 @@ # IEC61968 Common # ################### - # NOTE: location is tested separately due to constraints on the translation. - # "create_location": create_location(), + "create_location": create_location(), "create_organisation": create_organisation(), ##################################### @@ -225,21 +222,8 @@ "create_circuit": create_circuit(), } - -@pytest.mark.timeout(100000) -@given(**types_to_test) -@settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.large_base_example, HealthCheck.data_too_large], stateful_step_count=2) -def test_network_service_translations(**kwargs): - # - # NOTE: To prevent the `assume` required for the location from making this test take way too long, it has been separated out. - # - # If this test still appears to lock up, it is likely you have missed validating a class or forgot to exclude the table. Either figure out which - # case you have, or wait for the test to finish, and it will tell you. - # - # NOTE: updating hypothesis can break this test on python 3.9 to 1.200.0 and beyond, if you do that, and it breaks, this command - # will run only this test: - # tox -e py39 -- test/services/network/translator/test_network_translator.py::test_network_service_translations --no-cov - # +@pytest.mark.timeout(20000) +def test_network_service_translations(): validate_service_translations( NetworkService, NetworkServiceComparator(), @@ -272,31 +256,11 @@ def test_network_service_translations(**kwargs): TableProtectionRelayFunctionsSensors, TableRecloseDelays, - # Excluded location table in the other test. - TableLocations }, - **kwargs - ) - - -@given(location=create_location()) -@settings(suppress_health_check=[HealthCheck.too_slow]) -def test_network_service_translations_location(location: Location): - # If this `assume` is placed with the other checks it makes the test take a very long time to run due to the number of falsifying examples it creates. - assume_non_blank_street_address_details(location.main_address) - validate_service_translations( - NetworkService, - NetworkServiceComparator(), - NetworkDatabaseTables(), - excluded_tables={it.__class__ for it in NetworkDatabaseTables().tables if not isinstance(it, TableLocations)}, - location=location + types_to_test=types_to_test, ) - -# # NOTE: NameType is not sent via any grpc messages at this stage, so test it separately -# - def test_creates_new_name_type(): # noinspection PyArgumentList, PyUnresolvedReferences pb = NameType("nt1 name", "nt1 desc").to_pb() diff --git a/tox.ini b/tox.ini index dc276a09..c9749972 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{312, 311, 310, 39} + py{312, 311, 310} build [testenv] @@ -10,7 +10,7 @@ pip_pre = true extras = test depends = # build the package after testing - build: py{312, 311, 310, 39} + build: py{312, 311, 310} commands = pytest --cov=zepben.ewb --cov-report=xml --cov-branch {posargs} [testenv:build]