-
Notifications
You must be signed in to change notification settings - Fork 41
🐛 fix: ensure events only fire when public methods return valid data #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
Implement type-safe validation framework using Go generics: - Add generic validation system in validation.go with BaseValidator - Support for any SPINE data type with compile-time type safety - Comprehensive validation rules: RequireField, ValidateRange, ValidateEnum, etc. - Measurement-specific validation with predefined validators - Replace MeasurementWithMandatoryData with new validation approach - Complete GoDoc documentation with usage examples - Comprehensive test coverage for all validation scenarios - Markdown documentation guide for developers Benefits: - No reflection, direct field access for performance - Type-safe validators prevent runtime errors - Composable rules for complex validation scenarios - Clear error messages with validator context - Easy extension for new SPINE data types
- Create generic validation framework with Validator[T] interface - Implement BaseValidator with composable validation rules - Add MeasurementValidator for type-safe measurement validation - Unify validation through MeasurementPhaseSpecificDataForFilter - Remove non-spec frequency range validation (45-65Hz) - Add comprehensive test coverage for all validators - Achieve 100% test coverage for MPC use case The new validation system ensures spec compliance by: - Rejecting averageValue types when value is required - Rejecting empirical sources for energy measurements - Allowing any ValueState for current measurements - Validating voltage range (0-1000V) per spec - Removing frequency range check not in MPC spec This unified approach reduces code duplication and makes it easier to maintain consistent validation across all EEBUS use cases.
…function duplication - Replace local ptr[T any] functions with util.Ptr from spine-go utils package - Standardize "no valid data found" behavior to return errors consistently for both MPC and MGCP - Update MGCP test expectations to match consistent error handling - Remove duplicate utility functions across validator test files - Ensure both use cases handle data availability scenarios uniformly
Add validators for EG LPC (Energy Guard Load Power Consumption) use case to validate data structures when reading from Controllable Systems: - LoadControlLimitData: validate required fields (LimitId, Value) - DeviceConfigurationKeyValueData: validate required fields and value types - ElectricalConnectionCharacteristicData: validate required fields and characteristic types Validation follows EEBus LPC specification principles: - Only validate structural integrity and type requirements - Trust that CS has validated its own data according to spec - No arbitrary range validations beyond spec requirements This ensures data integrity when EG reads from CS devices while following the unified validation framework pattern established in MGCP and MPC use cases.
- Fix DeviceConfiguration OR/AND logic bug in CheckEventPayloadDataForFilter - Update all event handlers to validate data with corresponding public methods before firing events - Add public method validation to 18 event handlers across EG LPC, MA MPC, and MA MGCP use cases - Update event tests to provide complete valid data that passes both filters and validation - Ensure events are only triggered when data is actually retrievable via public API This prevents event notifications for data that would fail when accessed through public methods, ensuring consistency between event notifications and data availability.
…ctions - Rename 'min' and 'max' parameters to 'minVal' and 'maxVal' in validation functions - Fix CodeFactor warnings about redefinition of built-in functions - No functional changes, improves code maintainability
|
@sthelen-enqs this should also help for the scenario in your PR enbility/spine-go#52 |
- Increased internal/measurement.go coverage from 69% to 94.1% - Enhanced eg/lpc package coverage from 94.7% to 95.2% - Boosted ma/mgcp package coverage from 94.7% to 97.3% - Achieved 100% coverage for ma/mpc package (up from 97.4%) Added comprehensive test cases for: - Measurement validation functions and edge cases - LPC validator functions and configuration handling - MGCP/MPC event callbacks for phase-specific measurements - Error conditions and invalid data scenarios
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
I haven't fully checked through the whole PR, but found a couple of minor nits in the tests that might be nice to fix. I'll check over the rest of the validation later.
general nit: could you run gofmt on the test cases as well?
| assert.Nil(s.T(), fErr) | ||
|
|
||
| // Test 1: Missing LimitId - should return ErrDataInvalid when validator runs | ||
| // Note: Data must include Value to pass the `value.Value == nil` check in public.go:59 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is incorrect, and confusing. GetLimitDataForId will return nil, api.ErrDataNotAvailable because the filter won't match any limits and len(result) will be 0. The check on L59 then short-circuits on err != nil and value.Value will never be checked.
| // Note: Data must include Value to pass the `value.Value == nil` check in public.go:59 |
| // LimitId missing - this should trigger validation error | ||
| IsLimitChangeable: util.Ptr(true), | ||
| IsLimitActive: util.Ptr(false), | ||
| Value: model.NewScaledNumberType(6000), // Include value to pass initial check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Value: model.NewScaledNumberType(6000), // Include value to pass initial check | |
| Value: model.NewScaledNumberType(6000), |
| assert.Equal(s.T(), time.Duration(time.Hour*2), data) | ||
| } | ||
|
|
||
| func (s *EgLPCSuite) Test_ConsumptionNominalMax_ValidationErrors() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test case is using the wrong CharacteristicType throughout because the "current" version uses the DeviceType to determine the correct CharacterisicType, but the DeviceType is never initialised in the test suite (and therefore nil).
Could you maybe rebase this PR on fix/use-correct-eg-lpc-lpp-characteristic-types ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit I'm not in love with these changes. The validator logic feels more complicated than necessary, and I fear will make it easier to filter data incorrectly as evidenced by the nits and spec inconsistencies I've found so far.
I, however, do not have a better idea as of right now either.
| } | ||
|
|
||
| // Validate ElectricalConnectionCharacteristicData using EG LPC validator | ||
| // This ensures nominal consumption values are positive and within reasonable ranges |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it? As far as I can tell, it only validates that a characteristicId is set, and that the characteristicType is reasonable (which it has to be anyway since the filter uses the characterisicType to find the value in the first place.
Realistically this is only checking that CharacteristicId != nil (assuming our filter is functioning correctly).
Neither value nor unit are checked, therefore it's a bit of a stretch to say this ensures values are positive and within reasonable ranges.
| // Validate LoadControlLimitData using EG LPC validator | ||
| // This ensures data integrity and compliance with EG LPC specification requirements | ||
| if err := EGLPCLoadControlLimitValidator.Validate(value); err != nil { | ||
| resultErr = api.ErrDataInvalid | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does nothing that isn't already done by lines 58-61. It's not technically wrong, it's just dead code that's burning cycles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intention here to replace the current piecemeal validators with generic validators? In that case this should replace L59-61 and then I'd need to double-check the correctness again.
| // Validate DeviceConfigurationKeyValueData using EG LPC validator | ||
| // This ensures failsafe power limit values are within reasonable ranges and properly formatted | ||
| if err := EGLPCDeviceConfigurationValidator.Validate(data); err != nil { | ||
| return 0, api.ErrDataInvalid | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, except that in addition the comment is very misleading. This code doesn't actually check that power limit values are within reasonable limits. It only checks if KeyId is set (which must be set anyway if we've reached this function cal), and if a value is present that only ScaledNumber and/or Duration are set on it (but not String/Boolean/DateTime).
I'm not sure of the utility of this, and from a validation standpoint it's not even technically correct since:
a) data.Value must be set at this point (the code currently passes when it isn't),
b) I don't think the SPINE/EEBUS spec forbid having additional members of the DeviceConfigurationKeyValueValueType set at this point (such as DateTime, Boolean or String), and
c) if the spec did forbit it, the check would also need to be that ScaledNumber XOR Duration should be set on it since a failsafeConsumptionActivePowerLimit should not include a duration, and a failsafeDurationMinimum should not contain a scaledNumber
| // Validate validator parameter | ||
| if validator == nil { | ||
| return nil, fmt.Errorf("validator is required") | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make validator pass-by-value instead of pointer typed? Then it cannot be nil by type guarantee which reduces the risk of errors at runtime and (imo) makes it easier to reason about.
I don't think the struct is large enough that we have to worry about passing it by value, and I think the go compiler should just do the right thing™
Not a blocker in any way, just wondering if we can improve the ergonomics here.
| // For MGCP-003 compliance, we explicitly handle ErrSkipMeasurement | ||
| // All other validation errors also result in skipping the measurement | ||
| continue // Skip invalid measurements, don't fail entire operation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although the reasoning is correct, this error message is confusing as no error is handled explicitly.
It might be nice to have some way of logging/warning/reporting measurements with valueState != normal and probably another one for other validationErrors, but I don't know if we need that in this PR.
| // For MGCP-003 compliance, we explicitly handle ErrSkipMeasurement | |
| // All other validation errors also result in skipping the measurement | |
| continue // Skip invalid measurements, don't fail entire operation | |
| // Due to MGCP-003, and MPC-003 we want to skip measurements with valueState != normal | |
| // We also want to skip all other validation errors so that we don't fail the entire operation | |
| // just because the remote contains invalid data | |
| continue |
| WithRule(internal.RequireMeasurementId()). | ||
| WithRule(internal.RequireMeasurementValue()). | ||
| WithRule(internal.RequireValueType(model.MeasurementValueTypeTypeValue)). | ||
| WithRule(internal.RequireValueSource(currentSourceTypes...)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaict valueSource is always required for the MU and can be made mandatory here
| WithName("MPC Voltage"). | ||
| WithRule(internal.RequireMeasurementId()). | ||
| WithRule(internal.RequireMeasurementValue()). | ||
| WithRule(internal.RequireValueType(model.MeasurementValueTypeTypeValue)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaict valueSource is always required for the MU and can be made mandatory here
| WithRule(internal.RequireMeasurementId()). | ||
| WithRule(internal.RequireMeasurementValue()). | ||
| WithRule(internal.RequireValueType(model.MeasurementValueTypeTypeValue)). | ||
| WithRule(internal.RequireValueSource(frequencySourceTypes...)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaict valueSource is always required for the MU and can be made mandatory here
|
|
||
| // getMeasurementValue is a helper that validates and extracts the value from measurements | ||
| // DEPRECATED: Use MeasurementPhaseSpecificDataForFilter with validators instead | ||
| func getMeasurementValue(measurements []model.MeasurementDataType, validator *internal.MeasurementValidator) (float64, error) { | ||
| return internal.GetMeasurementValue(measurements, validator) | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is deprecated, can we just get rid of it?
| WithRule(internal.RequireMeasurementId()). | ||
| WithRule(internal.RequireMeasurementValue()). | ||
| WithRule(internal.RequireValueType(model.MeasurementValueTypeTypeValue)). | ||
| WithRule(internal.RequireValueSource(voltageSourceTypes...)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of MPC-003 shouldn't this be an equivalent SkipValueState as in mgcp/validators?
Summary
Changes Made
Core Logic Fix
Event Handler Validation
Test Coverage Improvements
Problem Solved
Previously, events could fire for data that would then fail when accessed through public methods due to validation failures. This created
inconsistency where users would receive event notifications but then get errors when trying to retrieve the announced data.
Solution
Events now perform the same validation as public methods before firing, ensuring that:
Test Coverage
Breaking Changes
None - this is a bug fix that improves reliability without changing public APIs.