Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/zepben/ewb/services/common/difference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion test/cim/cim_creators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
# 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

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():
Expand All @@ -36,15 +36,16 @@ 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():
pdrf = PanDemandResponseFunction(*pan_demand_response_function_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():
Expand Down
80 changes: 45 additions & 35 deletions test/services/common/translator/base_test_translator.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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 ""

Expand Down
6 changes: 2 additions & 4 deletions test/services/customer/translator/test_customer_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,8 +34,7 @@
}


@given(**types_to_test)
def test_customer_service_translations(**kwargs):
def test_customer_service_translations():
validate_service_translations(
CustomerService,
CustomerServiceComparator(),
Expand All @@ -45,7 +43,7 @@ def test_customer_service_translations(**kwargs):
TableCustomerAgreementsPricingStructures,
TablePricingStructuresTariffs
},
**kwargs
types_to_test=types_to_test,
)


Expand Down
6 changes: 2 additions & 4 deletions test/services/diagram/translator/test_diagram_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,16 +25,15 @@
}


@given(**types_to_test)
def test_diagram_service_translations(**kwargs):
def test_diagram_service_translations():
validate_service_translations(
DiagramService,
DiagramServiceComparator(),
DiagramDatabaseTables(),
excluded_tables={
TableDiagramObjectPoints
},
**kwargs
types_to_test=types_to_test,
)


Expand Down
50 changes: 7 additions & 43 deletions test/services/network/translator/test_network_translator.py
Original file line number Diff line number Diff line change
@@ -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, \
Expand Down Expand Up @@ -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(),

#####################################
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py{312, 311, 310, 39}
py{312, 311, 310}
build

[testenv]
Expand All @@ -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]
Expand Down