Skip to content

Commit

Permalink
Add an MTRDevice method for checking whether a data-value satisfies s…
Browse files Browse the repository at this point in the history
…ome expectation. (project-chip#36151)
  • Loading branch information
bzbarsky-apple authored Oct 22, 2024
1 parent f32d005 commit e36bde7
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 20 deletions.
162 changes: 162 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ - (BOOL)deviceCachePrimed
return NO;
}

#pragma mark - Suspend/resume management

- (void)controllerSuspended
{
// Nothing to do for now.
Expand All @@ -549,6 +551,166 @@ - (void)controllerResumed
// Nothing to do for now.
}

#pragma mark - Value comparisons

- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)one isEqualToDataValue:(MTRDeviceDataValueDictionary)theOther
{
// Sanity check for nil cases
if (!one && !theOther) {
MTR_LOG_ERROR("%@ attribute data-value comparison does not expect comparing two nil dictionaries", self);
return YES;
}
if (!one || !theOther) {
// Comparing against nil is expected, and should return NO quietly
return NO;
}

// Attribute data-value dictionaries are equal if type and value are equal, and specifically, this should return true if values are both nil
return [one[MTRTypeKey] isEqual:theOther[MTRTypeKey]] && ((one[MTRValueKey] == theOther[MTRValueKey]) || [one[MTRValueKey] isEqual:theOther[MTRValueKey]]);
}

// _attributeDataValue:satisfiesExpectedDataValue: checks whether the newly
// received attribute data value satisfies the expectation we have.
//
// For now, a value is considered to satisfy the expectation if it's equal to
// the expected value, though we allow the fields of structs to be in a
// different order than expected: while in theory the spec does require a
// specific ordering for struct fields, in practice we should not force certain
// API consumers to deal with knowing what that ordering is.
//
// Things to consider for future:
//
// 1) Should a value that has _extra_ fields in a struct compared to the expected
// value be considered as satisfying the expectation? Arguably, yes.
//
// 2) Should lists actually enforce order (as now), or should they allow
// reordering entries?
//
// 3) For fabric-scoped lists, should we have a way to check for just "our
// fabric's" entries?
- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)observed satisfiesValueExpectation:(MTRDeviceDataValueDictionary)expected
{
// Sanity check for nil cases (which really should not happen!)
if (!observed && !expected) {
MTR_LOG_ERROR("%@ observed to expected attribute data-value comparison does not expect comparing two nil dictionaries", self);
return YES;
}

if (!observed || !expected) {
// Again, not expected here. But clearly the expectation is not really
// satisfied, in some sense.
MTR_LOG_ERROR("@ observed to expected attribute data-value comparison does not expect a nil %s", observed ? "expected" : "observed");
return NO;
}

if (![observed[MTRTypeKey] isEqual:expected[MTRTypeKey]]) {
// Different types, does not satisfy expectation.
return NO;
}

if ([MTRArrayValueType isEqual:expected[MTRTypeKey]]) {
// For array-values, check that sizes are same and entries satisfy expectations.
if (![observed[MTRValueKey] isKindOfClass:NSArray.class] || ![expected[MTRValueKey] isKindOfClass:NSArray.class]) {
// Malformed data, just claim expectation is not satisfied.
MTR_LOG_ERROR("%@ at least one of observed and expected value is not an NSArrray: %@, %@", self, observed, expected);
return NO;
}

NSArray<NSDictionary<NSString *, MTRDeviceDataValueDictionary> *> * observedArray = observed[MTRValueKey];
NSArray<NSDictionary<NSString *, MTRDeviceDataValueDictionary> *> * expectedArray = expected[MTRValueKey];

if (observedArray.count != expectedArray.count) {
return NO;
}

for (NSUInteger i = 0; i < observedArray.count; ++i) {
NSDictionary<NSString *, MTRDeviceDataValueDictionary> * observedEntry = observedArray[i];
NSDictionary<NSString *, MTRDeviceDataValueDictionary> * expectedEntry = expectedArray[i];

if (![observedEntry isKindOfClass:NSDictionary.class] || ![expectedEntry isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("%@ expected or observed array-value contains entries that are not NSDictionary: %@, %@", self, observedEntry, expectedEntry);
return NO;
}

if (![self _attributeDataValue:observedEntry[MTRDataKey] satisfiesValueExpectation:expectedEntry[MTRDataKey]]) {
return NO;
}
}

return YES;
}

if (![MTRStructureValueType isEqual:expected[MTRTypeKey]]) {
// For everything except arrays and structs, expectation is satisfied
// exactly when the values are equal.
return [self _attributeDataValue:observed isEqualToDataValue:expected];
}

// Now we have two structure-values. Make sure they have the same number of fields
// in them.
if (![observed[MTRValueKey] isKindOfClass:NSArray.class] || ![expected[MTRValueKey] isKindOfClass:NSArray.class]) {
// Malformed data, just claim not equivalent.
MTR_LOG_ERROR("%@ at least one of observed and expected value is not an NSArrray: %@, %@", self, observed, expected);
return NO;
}

NSArray<NSDictionary<NSString *, id> *> * observedArray = observed[MTRValueKey];
NSArray<NSDictionary<NSString *, id> *> * expectedArray = expected[MTRValueKey];

if (observedArray.count != expectedArray.count) {
return NO;
}

for (NSDictionary<NSString *, id> * expectedField in expectedArray) {
if (![expectedField[MTRContextTagKey] isKindOfClass:NSNumber.class] || ![expectedField[MTRDataKey] isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("%@ expected structure-value contains invalid field %@", self, expectedField);
return NO;
}

NSNumber * expectedContextTag = expectedField[MTRContextTagKey];

// Make sure it's present in the other array. In practice, these are
// pretty small arrays, so the O(N^2) behavior here is ok.
BOOL found = NO;
for (NSDictionary<NSString *, id> * observedField in observedArray) {
if (![observedField[MTRContextTagKey] isKindOfClass:NSNumber.class] || ![observedField[MTRDataKey] isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("%@ observed structure-value contains invalid field %@", self, observedField);
return NO;
}

NSNumber * observedContextTag = observedField[MTRContextTagKey];
if ([expectedContextTag isEqual:observedContextTag]) {
found = YES;

// Compare the data.
if (![self _attributeDataValue:observedField[MTRDataKey] satisfiesValueExpectation:expectedField[MTRDataKey]]) {
return NO;
}

// Found a match for the context tag, stop looking.
break;
}
}

if (!found) {
// Context tag present in expected but not observed.
return NO;
}
}

// All entries in the first field array matched entries in the second field
// array. Since the lengths are equal, the two arrays must match, as long
// as all the context tags listed are distinct. If someone produces invalid
// TLV with the same context tag set in it multiple times, this method could
// claim two structure-values are equivalent when the first has two fields
// with context tag N and the second has a field with context tag N and
// another field with context tag M. That should be ok, in practice, but if
// we discover it's not we will need a better algorithm here. It's not
// clear what "equivalent" should mean for such malformed TLV, expecially if
// the same context tag maps to different values in one of the structs.
return YES;
}

@end

/* BEGIN DRAGONS: Note methods here cannot be renamed, and are used by private callers, do not rename, remove or modify behavior here */
Expand Down
3 changes: 3 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceDataValueDictionary.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

NS_ASSUME_NONNULL_BEGIN

/**
* A data-value as defined in MTRBaseDevice.h.
*/
typedef NSDictionary<NSString *, id> * MTRDeviceDataValueDictionary;

NS_ASSUME_NONNULL_END
22 changes: 3 additions & 19 deletions src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3307,22 +3307,6 @@ - (void)_performScheduledExpirationCheck
return nil;
}

- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther
{
// Sanity check for nil cases
if (!one && !theOther) {
MTR_LOG_ERROR("%@ attribute data-value comparison does not expect comparing two nil dictionaries", self);
return YES;
}
if (!one || !theOther) {
// Comparing against nil is expected, and should return NO quietly
return NO;
}

// Attribute data-value dictionaries are equal if type and value are equal, and specifically, this should return true if values are both nil
return [one[MTRTypeKey] isEqual:theOther[MTRTypeKey]] && ((one[MTRValueKey] == theOther[MTRValueKey]) || [one[MTRValueKey] isEqual:theOther[MTRValueKey]]);
}

// Utility to return data value dictionary without data version
- (NSDictionary *)_dataValueWithoutDataVersion:(NSDictionary *)attributeValue
{
Expand Down Expand Up @@ -3538,9 +3522,9 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray<NSDictionary<NSSt
NSMutableArray * attributePathsToReport = [NSMutableArray array];
for (NSDictionary<NSString *, id> * attributeResponseValue in reportedAttributeValues) {
MTRAttributePath * attributePath = attributeResponseValue[MTRAttributePathKey];
NSDictionary * attributeDataValue = attributeResponseValue[MTRDataKey];
NSError * attributeError = attributeResponseValue[MTRErrorKey];
NSDictionary * previousValue;
MTRDeviceDataValueDictionary _Nullable attributeDataValue = attributeResponseValue[MTRDataKey];
NSError * _Nullable attributeError = attributeResponseValue[MTRErrorKey];
MTRDeviceDataValueDictionary _Nullable previousValue;

// sanity check either data value or error must exist
if (!attributeDataValue && !attributeError) {
Expand Down
5 changes: 5 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#import "MTRAsyncWorkQueue.h"
#import "MTRDefines_Internal.h"
#import "MTRDeviceDataValueDictionary.h"
#import "MTRDeviceStorageBehaviorConfiguration_Internal.h"

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -164,6 +165,10 @@ MTR_DIRECT_MEMBERS
- (void)controllerSuspended;
- (void)controllerResumed;

// Methods for comparing attribute data values.
- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)one isEqualToDataValue:(MTRDeviceDataValueDictionary)theOther;
- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)observed satisfiesValueExpectation:(MTRDeviceDataValueDictionary)expected;

@end

#pragma mark - MTRDevice internal state monitoring
Expand Down
Loading

0 comments on commit e36bde7

Please sign in to comment.