Skip to content

Commit 0b8ffb7

Browse files
andy31415andreilitvinbzbarsky-applerestyled-commitstcarmelveilleux
authoredSep 18, 2024··
Make use of DataModel::Provider in writes (project-chip#34754)
* Implement DM::Provider::Write usage * Fix compile * Fix java builds * Update src/app/InteractionModelEngine.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Make codegen data model call increasing the cluster data version * Restyle * Make sure that attribute changed information can be propagated out of ember * Optimize storage size of WriteHandler * Restyle * Make the code more obvious identical with what was there before * Change Provider to not double-call the dirty and version increase * Update src/app/InteractionModelEngine.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Cleaner usage: no need of a separate function that is used in one place only * Attempt an API update * Fix typos in the Accessors src * Fix typo and regen * More fixes on accessors * Update signature for emAfWriteAttributeExternal * Add a comment about all the checks being vague * Update src/app/util/af-types.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/af-types.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-storage.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/zap-templates/templates/app/attributes/Accessors-src.zapt Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/mock/CodegenEmberMocks.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/zap-templates/templates/app/attributes/Accessors-src.zapt Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/zap-templates/templates/app/attributes/Accessors-src.zapt Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * zap regen and restyle * Update src/app/zap-templates/templates/app/attributes/Accessors-src.zapt Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-table.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-storage.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-storage.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-table.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-table.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-storage.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/util/attribute-table.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Rename ChangedPathListener to AttributesChangedListener * Remove chip:: and chip::app * Update constructors of AttributePathParams and add nodiscard according to the linter to never call const methods without considering their return value * Restyled by clang-format * Remove auto-inserted include * Update again and zap regen: removed extra namespace prefixes in accessors.h/cpp * Add comment about uint8_t non-const usage... * Another rename given that the listener is now an attributes and not a path listener * Update src/app/util/attribute-table.h Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com> * Update after merge to have more things compile * Everything compiles now * Restyle * Make unit tests pass: mocks also have to call the change listeners * Comment update to talk more about AttributesChangedListener * Restyle * Add support for a previous path write ... this is similar to what ACL caching and tokenizing currently does, but in a explicit manner describing the ACL use case * Remove some extra added includes * Another include fix * Follow the comment and do not restrict the cache to ACL cluster, since this is what current ember does * More self code review changes * Make tests pass, more code cleanup * Match ordering to ember-compatibility functions. This is ODD because we check attribute existance before access! * Adjust test ordering and add a large note that we are probably doing the wrong thing, however tests force us to do the wrong thing * Fix unit test * Remove odd comment * Add a few ending endifs * Renamed ScopedExchangeContext * Update src/app/InteractionModelEngine.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/WriteHandler.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/WriteHandler.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/data-model-provider/OperationTypes.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/data-model-provider/OperationTypes.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/data-model-provider/OperationTypes.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Restyle to reorder includes * Add issue link * Update equality logic * Rename member to mLastSuccessfullyWrittenPath * Update argument logic * Make ActionContext private in IME so it is not such a public API * Clean up comments * fix up compares * Restyle --------- Co-authored-by: Andrei Litvin <andreilitvin@google.com> Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> Co-authored-by: Restyled.io <commits@restyled.io> Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
1 parent 2396bb4 commit 0b8ffb7

20 files changed

+513
-77
lines changed
 

‎src/app/AttributeValueDecoder.h

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class AttributeValueDecoder
6565
const Access::SubjectDescriptor & GetSubjectDescriptor() const { return mSubjectDescriptor; }
6666

6767
private:
68+
friend class TestOnlyAttributeValueDecoderAccessor;
69+
6870
TLV::TLVReader & mReader;
6971
bool mTriedDecode = false;
7072
const Access::SubjectDescriptor mSubjectDescriptor;

‎src/app/EventManagement.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,12 @@ void EventManagement::SetScheduledEventInfo(EventNumber & aEventNumber, uint32_t
855855
aInitialWrittenEventBytes = mBytesWritten;
856856
}
857857

858+
CHIP_ERROR EventManagement::GenerateEvent(EventLoggingDelegate * eventPayloadWriter, const EventOptions & options,
859+
EventNumber & generatedEventNumber)
860+
{
861+
return LogEvent(eventPayloadWriter, options, generatedEventNumber);
862+
}
863+
858864
void CircularEventBuffer::Init(uint8_t * apBuffer, uint32_t aBufferLength, CircularEventBuffer * apPrev,
859865
CircularEventBuffer * apNext, PriorityLevel aPriorityLevel)
860866
{
@@ -914,5 +920,6 @@ CHIP_ERROR CircularEventBufferWrapper::GetNextBuffer(TLVReader & aReader, const
914920
exit:
915921
return err;
916922
}
923+
917924
} // namespace app
918925
} // namespace chip

‎src/app/EventManagement.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <app/EventLoggingTypes.h>
3232
#include <app/MessageDef/EventDataIB.h>
3333
#include <app/MessageDef/StatusIB.h>
34+
#include <app/data-model-provider/EventsGenerator.h>
3435
#include <app/util/basic-types.h>
3536
#include <lib/core/TLVCircularBuffer.h>
3637
#include <lib/support/CHIPCounter.h>
@@ -196,7 +197,7 @@ struct LogStorageResources
196197
* more space for new events.
197198
*/
198199

199-
class EventManagement
200+
class EventManagement : public DataModel::EventsGenerator
200201
{
201202
public:
202203
/**
@@ -387,6 +388,10 @@ class EventManagement
387388
*/
388389
void SetScheduledEventInfo(EventNumber & aEventNumber, uint32_t & aInitialWrittenEventBytes) const;
389390

391+
/* EventsGenerator implementation */
392+
CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventPayloadWriter, const EventOptions & options,
393+
EventNumber & generatedEventNumber) override;
394+
390395
private:
391396
/**
392397
* @brief
@@ -559,5 +564,6 @@ class EventManagement
559564

560565
System::Clock::Milliseconds64 mMonotonicStartupTime;
561566
};
567+
562568
} // namespace app
563569
} // namespace chip

‎src/app/InteractionModelEngine.cpp

+37-6
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,7 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnWriteRequest(Messa
895895
{
896896
if (writeHandler.IsFree())
897897
{
898-
VerifyOrReturnError(writeHandler.Init(this) == CHIP_NO_ERROR, Status::Busy);
898+
VerifyOrReturnError(writeHandler.Init(GetDataModelProvider(), this) == CHIP_NO_ERROR, Status::Busy);
899899
return writeHandler.OnWriteRequest(apExchangeContext, std::move(aPayload), aIsTimedWrite);
900900
}
901901
}
@@ -996,6 +996,9 @@ CHIP_ERROR InteractionModelEngine::OnMessageReceived(Messaging::ExchangeContext
996996

997997
Protocols::InteractionModel::Status status = Status::Failure;
998998

999+
// Ensure that DataModel::Provider has access to the exchange the message was received on.
1000+
CurrentExchangeValueScope scopedExchangeContext(*this, apExchangeContext);
1001+
9991002
// Group Message can only be an InvokeCommandRequest or WriteRequest
10001003
if (apExchangeContext->IsGroupExchangeContext() &&
10011004
!aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::InvokeCommandRequest) &&
@@ -1749,16 +1752,44 @@ DataModel::Provider * InteractionModelEngine::SetDataModelProvider(DataModel::Pr
17491752
// Alternting data model should not be done while IM is actively handling requests.
17501753
VerifyOrDie(mReadHandlers.begin() == mReadHandlers.end());
17511754

1752-
DataModel::Provider * oldModel = GetDataModelProvider();
1753-
mDataModelProvider = model;
1755+
DataModel::Provider * oldModel = mDataModelProvider;
1756+
if (oldModel != nullptr)
1757+
{
1758+
CHIP_ERROR err = oldModel->Shutdown();
1759+
if (err != CHIP_NO_ERROR)
1760+
{
1761+
ChipLogError(InteractionModel, "Failure on interaction model shutdown: %" CHIP_ERROR_FORMAT, err.Format());
1762+
}
1763+
}
1764+
1765+
mDataModelProvider = model;
1766+
if (mDataModelProvider != nullptr)
1767+
{
1768+
DataModel::InteractionModelContext context;
1769+
1770+
context.eventsGenerator = &EventManagement::GetInstance();
1771+
context.dataModelChangeListener = &mReportingEngine;
1772+
context.actionContext = this;
1773+
1774+
CHIP_ERROR err = mDataModelProvider->Startup(context);
1775+
if (err != CHIP_NO_ERROR)
1776+
{
1777+
ChipLogError(InteractionModel, "Failure on interaction model startup: %" CHIP_ERROR_FORMAT, err.Format());
1778+
}
1779+
}
1780+
17541781
return oldModel;
17551782
}
17561783

1757-
DataModel::Provider * InteractionModelEngine::GetDataModelProvider() const
1784+
DataModel::Provider * InteractionModelEngine::GetDataModelProvider()
17581785
{
17591786
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
1760-
// TODO: this should be temporary, we should fully inject the data model
1761-
VerifyOrReturnValue(mDataModelProvider != nullptr, CodegenDataModelProviderInstance());
1787+
if (mDataModelProvider == nullptr)
1788+
{
1789+
// These should be called within the CHIP processing loop.
1790+
assertChipStackLockedByCurrentThread();
1791+
SetDataModelProvider(CodegenDataModelProviderInstance());
1792+
}
17621793
#endif
17631794
return mDataModelProvider;
17641795
}

‎src/app/InteractionModelEngine.h

+25-2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ namespace app {
8686
*/
8787
class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
8888
public Messaging::ExchangeDelegate,
89+
private DataModel::ActionContext,
8990
public CommandResponseSender::Callback,
9091
public CommandHandlerImpl::Callback,
9192
public ReadHandler::ManagementCallback,
@@ -402,7 +403,10 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
402403
}
403404
#endif
404405

405-
DataModel::Provider * GetDataModelProvider() const;
406+
// Temporarily NOT const because the data model provider will be auto-set
407+
// to codegen on first usage. This behaviour will be changed once each
408+
// application must explicitly set the data model provider.
409+
DataModel::Provider * GetDataModelProvider();
406410

407411
// MUST NOT be used while the interaction model engine is running as interaction
408412
// model functionality (e.g. active reads/writes/subscriptions) rely on data model
@@ -412,6 +416,9 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
412416
DataModel::Provider * SetDataModelProvider(DataModel::Provider * model);
413417

414418
private:
419+
/* DataModel::ActionContext implementation */
420+
Messaging::ExchangeContext * CurrentExchange() override { return mCurrentExchange; }
421+
415422
friend class reporting::Engine;
416423
friend class TestCommandInteraction;
417424
friend class TestInteractionModelEngine;
@@ -698,7 +705,23 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
698705

699706
SubscriptionResumptionStorage * mpSubscriptionResumptionStorage = nullptr;
700707

701-
DataModel::Provider * mDataModelProvider = nullptr;
708+
DataModel::Provider * mDataModelProvider = nullptr;
709+
Messaging::ExchangeContext * mCurrentExchange = nullptr;
710+
711+
// Changes the current exchange context of a InteractionModelEngine to a given context
712+
class CurrentExchangeValueScope
713+
{
714+
public:
715+
CurrentExchangeValueScope(InteractionModelEngine & engine, Messaging::ExchangeContext * context) : mEngine(engine)
716+
{
717+
mEngine.mCurrentExchange = context;
718+
}
719+
720+
~CurrentExchangeValueScope() { mEngine.mCurrentExchange = nullptr; }
721+
722+
private:
723+
InteractionModelEngine & mEngine;
724+
};
702725
};
703726

704727
} // namespace app

‎src/app/WriteHandler.cpp

+48-4
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,40 @@
1818

1919
#include <app/AppConfig.h>
2020
#include <app/AttributeAccessInterfaceRegistry.h>
21+
#include <app/AttributeValueDecoder.h>
2122
#include <app/InteractionModelEngine.h>
2223
#include <app/MessageDef/EventPathIB.h>
2324
#include <app/MessageDef/StatusIB.h>
2425
#include <app/StatusResponse.h>
2526
#include <app/WriteHandler.h>
27+
#include <app/data-model-provider/OperationTypes.h>
2628
#include <app/reporting/Engine.h>
2729
#include <app/util/MatterCallbacks.h>
2830
#include <app/util/ember-compatibility-functions.h>
2931
#include <credentials/GroupDataProvider.h>
32+
#include <lib/core/CHIPError.h>
3033
#include <lib/support/CodeUtils.h>
3134
#include <lib/support/TypeTraits.h>
35+
#include <messaging/ExchangeContext.h>
3236
#include <protocols/interaction_model/StatusCode.h>
3337

38+
#include <optional>
39+
3440
namespace chip {
3541
namespace app {
3642

3743
using namespace Protocols::InteractionModel;
3844
using Status = Protocols::InteractionModel::Status;
3945
constexpr uint8_t kListAttributeType = 0x48;
4046

41-
CHIP_ERROR WriteHandler::Init(WriteHandlerDelegate * apWriteHandlerDelegate)
47+
CHIP_ERROR WriteHandler::Init(DataModel::Provider * apProvider, WriteHandlerDelegate * apWriteHandlerDelegate)
4248
{
4349
VerifyOrReturnError(!mExchangeCtx, CHIP_ERROR_INCORRECT_STATE);
4450
VerifyOrReturnError(apWriteHandlerDelegate, CHIP_ERROR_INVALID_ARGUMENT);
51+
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
52+
VerifyOrReturnError(apProvider, CHIP_ERROR_INVALID_ARGUMENT);
53+
mDataModelProvider = apProvider;
54+
#endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
4555

4656
mDelegate = apWriteHandlerDelegate;
4757
MoveToState(State::Initialized);
@@ -63,6 +73,9 @@ void WriteHandler::Close()
6373
DeliverFinalListWriteEnd(false /* wasSuccessful */);
6474
mExchangeCtx.Release();
6575
mStateFlags.Clear(StateBits::kSuppressResponse);
76+
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
77+
mDataModelProvider = nullptr;
78+
#endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
6679
MoveToState(State::Uninitialized);
6780
}
6881

@@ -354,7 +367,7 @@ CHIP_ERROR WriteHandler::ProcessAttributeDataIBs(TLV::TLVReader & aAttributeData
354367
err = CHIP_NO_ERROR;
355368
}
356369
SuccessOrExit(err);
357-
err = WriteSingleClusterData(subjectDescriptor, dataAttributePath, dataReader, this);
370+
err = WriteClusterData(subjectDescriptor, dataAttributePath, dataReader);
358371
if (err != CHIP_NO_ERROR)
359372
{
360373
mWriteResponseBuilder.GetWriteResponses().Rollback(backup);
@@ -501,7 +514,7 @@ CHIP_ERROR WriteHandler::ProcessGroupAttributeDataIBs(TLV::TLVReader & aAttribut
501514

502515
DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Write,
503516
DataModelCallbacks::OperationOrder::Pre, dataAttributePath);
504-
err = WriteSingleClusterData(subjectDescriptor, dataAttributePath, tmpDataReader, this);
517+
err = WriteClusterData(subjectDescriptor, dataAttributePath, tmpDataReader);
505518
if (err != CHIP_NO_ERROR)
506519
{
507520
ChipLogError(DataManagement,
@@ -552,14 +565,18 @@ Status WriteHandler::ProcessWriteRequest(System::PacketBufferHandle && aPayload,
552565
// our callees hand out Status as well.
553566
Status status = Status::InvalidAction;
554567

568+
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
569+
mLastSuccessfullyWrittenPath = std::nullopt;
570+
#endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
571+
555572
reader.Init(std::move(aPayload));
556573

557574
err = writeRequestParser.Init(reader);
558575
SuccessOrExit(err);
559576

560577
#if CHIP_CONFIG_IM_PRETTY_PRINT
561578
writeRequestParser.PrettyPrint();
562-
#endif
579+
#endif // CHIP_CONFIG_IM_PRETTY_PRINT
563580
bool boolValue;
564581

565582
boolValue = mStateFlags.Has(StateBits::kSuppressResponse);
@@ -703,5 +720,32 @@ void WriteHandler::MoveToState(const State aTargetState)
703720
ChipLogDetail(DataManagement, "IM WH moving to [%s]", GetStateStr());
704721
}
705722

723+
CHIP_ERROR WriteHandler::WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath,
724+
TLV::TLVReader & aData)
725+
{
726+
// Writes do not have a checked-path. If data model interface is enabled (both checked and only version)
727+
// the write is done via the DataModel interface
728+
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
729+
VerifyOrReturnError(mDataModelProvider != nullptr, CHIP_ERROR_INCORRECT_STATE);
730+
731+
DataModel::WriteAttributeRequest request;
732+
733+
request.path = aPath;
734+
request.subjectDescriptor = aSubject;
735+
request.previousSuccessPath = mLastSuccessfullyWrittenPath;
736+
request.writeFlags.Set(DataModel::WriteFlags::kTimed, IsTimedWrite());
737+
738+
AttributeValueDecoder decoder(aData, aSubject);
739+
740+
DataModel::ActionReturnStatus status = mDataModelProvider->WriteAttribute(request, decoder);
741+
742+
mLastSuccessfullyWrittenPath = status.IsSuccess() ? std::make_optional(aPath) : std::nullopt;
743+
744+
return AddStatusInternal(aPath, StatusIB(status.GetStatusCode()));
745+
#else
746+
return WriteSingleClusterData(aSubject, aPath, aData, this);
747+
#endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
748+
}
749+
706750
} // namespace app
707751
} // namespace chip

‎src/app/WriteHandler.h

+13-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
#include <app/AppConfig.h>
2222
#include <app/AttributeAccessToken.h>
2323
#include <app/AttributePathParams.h>
24+
#include <app/ConcreteAttributePath.h>
2425
#include <app/InteractionModelDelegatePointers.h>
2526
#include <app/MessageDef/WriteResponseMessage.h>
27+
#include <app/data-model-provider/Provider.h>
2628
#include <lib/core/CHIPCore.h>
2729
#include <lib/core/TLVDebug.h>
2830
#include <lib/support/BitFlags.h>
@@ -69,6 +71,7 @@ class WriteHandler : public Messaging::ExchangeDelegate
6971
* construction until a call to Close is made to terminate the
7072
* instance.
7173
*
74+
* @param[in] apProvider A valid pointer to the model used to forward writes towards
7275
* @param[in] apWriteHandlerDelegate A Valid pointer to the WriteHandlerDelegate.
7376
*
7477
* @retval #CHIP_ERROR_INVALID_ARGUMENT on invalid pointers
@@ -77,7 +80,7 @@ class WriteHandler : public Messaging::ExchangeDelegate
7780
* @retval #CHIP_NO_ERROR On success.
7881
*
7982
*/
80-
CHIP_ERROR Init(WriteHandlerDelegate * apWriteHandlerDelegate);
83+
CHIP_ERROR Init(DataModel::Provider * apProvider, WriteHandlerDelegate * apWriteHandlerDelegate);
8184

8285
/**
8386
* Process a write request. Parts of the processing may end up being asynchronous, but the WriteHandler
@@ -182,11 +185,20 @@ class WriteHandler : public Messaging::ExchangeDelegate
182185
System::PacketBufferHandle && aPayload) override;
183186
void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override;
184187

188+
// Write the given data to the given path
189+
CHIP_ERROR WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath,
190+
TLV::TLVReader & aData);
191+
185192
Messaging::ExchangeHolder mExchangeCtx;
186193
WriteResponseMessage::Builder mWriteResponseBuilder;
187194
Optional<ConcreteAttributePath> mProcessingAttributePath;
188195
Optional<AttributeAccessToken> mACLCheckCache = NullOptional;
189196

197+
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
198+
DataModel::Provider * mDataModelProvider = nullptr;
199+
std::optional<ConcreteAttributePath> mLastSuccessfullyWrittenPath;
200+
#endif
201+
190202
// This may be a "fake" pointer or a real delegate pointer, depending
191203
// on CHIP_CONFIG_STATIC_GLOBAL_INTERACTION_MODEL_ENGINE setting.
192204
//

‎src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp

+81-38
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717
#include <app/codegen-data-model-provider/CodegenDataModelProvider.h>
18+
#include <app/util/attribute-storage.h>
1819

1920
#include <access/AccessControl.h>
2021
#include <app-common/zap-generated/attribute-type.h>
@@ -45,6 +46,17 @@ namespace {
4546
using namespace chip::app::Compatibility::Internal;
4647
using Protocols::InteractionModel::Status;
4748

49+
class ContextAttributesChangeListener : public AttributesChangedListener
50+
{
51+
public:
52+
ContextAttributesChangeListener(const DataModel::InteractionModelContext & context) : mListener(context.dataModelChangeListener)
53+
{}
54+
void MarkDirty(const AttributePathParams & path) override { mListener->MarkDirty(path); }
55+
56+
private:
57+
DataModel::ProviderChangeListener * mListener;
58+
};
59+
4860
/// Attempts to write via an attribute access interface (AAI)
4961
///
5062
/// If it returns a CHIP_ERROR, then this is a FINAL result (i.e. either failure or success)
@@ -273,27 +285,14 @@ DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const Dat
273285
ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI,
274286
ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId));
275287

276-
// ACL check for non-internal requests
277-
if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
278-
{
279-
ReturnErrorCodeIf(!request.subjectDescriptor.has_value(), Status::UnsupportedAccess);
280-
281-
Access::RequestPath requestPath{ .cluster = request.path.mClusterId,
282-
.endpoint = request.path.mEndpointId,
283-
.requestType = Access::RequestType::kAttributeWriteRequest,
284-
.entityId = request.path.mAttributeId };
285-
CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath,
286-
RequiredPrivilege::ForWriteAttribute(request.path));
287-
288-
if (err != CHIP_NO_ERROR)
289-
{
290-
ReturnErrorCodeIf((err != CHIP_ERROR_ACCESS_DENIED) && (err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL), err);
291-
292-
// TODO: when wildcard/group writes are supported, handle them to discard rather than fail with status
293-
return err == CHIP_ERROR_ACCESS_DENIED ? Status::UnsupportedAccess : Status::AccessRestricted;
294-
}
295-
}
296-
288+
// TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however
289+
// existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess
290+
//
291+
// This should likely be fixed in spec (probably already fixed by
292+
// https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024)
293+
// and tests and implementation
294+
//
295+
// Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735
297296
auto metadata = Ember::FindAttributeMetadata(request.path);
298297

299298
// Explicit failure in finding a suitable metadata
@@ -322,7 +321,56 @@ DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const Dat
322321
if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
323322
{
324323
VerifyOrReturnError(!isReadOnly, Status::UnsupportedWrite);
324+
}
325+
326+
// ACL check for non-internal requests
327+
bool checkAcl = !request.operationFlags.Has(DataModel::OperationFlags::kInternal);
325328

329+
// For chunking, ACL check is not re-done if the previous write was successful for the exact same
330+
// path. We apply this everywhere as a shortcut, although realistically this is only for AccessControl cluster
331+
if (checkAcl && request.previousSuccessPath.has_value())
332+
{
333+
// NOTE: explicit cast/check only for attribute path and nothing else.
334+
//
335+
// In particular `request.path` is a DATA path (contains a list index)
336+
// and we do not want request.previousSuccessPath to be auto-cast to a
337+
// data path with a empty list and fail the compare.
338+
//
339+
// This could be `request.previousSuccessPath != request.path` (where order
340+
// is important) however that would seem more brittle (relying that a != b
341+
// behaves differently than b != a due to casts). Overall Data paths are not
342+
// the same as attribute paths.
343+
//
344+
// Also note that Concrete path have a mExpanded that is not used in compares.
345+
const ConcreteAttributePath & attributePathA = request.path;
346+
const ConcreteAttributePath & attributePathB = *request.previousSuccessPath;
347+
348+
checkAcl = (attributePathA != attributePathB);
349+
}
350+
351+
if (checkAcl)
352+
{
353+
ReturnErrorCodeIf(!request.subjectDescriptor.has_value(), Status::UnsupportedAccess);
354+
355+
Access::RequestPath requestPath{ .cluster = request.path.mClusterId,
356+
.endpoint = request.path.mEndpointId,
357+
.requestType = Access::RequestType::kAttributeWriteRequest,
358+
.entityId = request.path.mAttributeId };
359+
CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath,
360+
RequiredPrivilege::ForWriteAttribute(request.path));
361+
362+
if (err != CHIP_NO_ERROR)
363+
{
364+
VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess);
365+
VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted);
366+
367+
return err;
368+
}
369+
}
370+
371+
// Internal is allowed to bypass timed writes and read-only.
372+
if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
373+
{
326374
VerifyOrReturnError(!(*attributeMetadata)->MustUseTimedWrite() || request.writeFlags.Has(DataModel::WriteFlags::kTimed),
327375
Status::NeedsTimedInteraction);
328376
}
@@ -349,18 +397,18 @@ DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const Dat
349397
}
350398
}
351399

400+
ContextAttributesChangeListener change_listener(CurrentContext());
401+
352402
AttributeAccessInterface * aai =
353403
AttributeAccessInterfaceRegistry::Instance().Get(request.path.mEndpointId, request.path.mClusterId);
354404
std::optional<CHIP_ERROR> aai_result = TryWriteViaAccessInterface(request.path, aai, decoder);
355405
if (aai_result.has_value())
356406
{
357407
if (*aai_result == CHIP_NO_ERROR)
358408
{
359-
// TODO: change callbacks should likely be routed through the context `MarkDirty` only
360-
// however for now this is called directly because ember code does this call
361-
// inside emberAfWriteAttribute.
362-
MatterReportingAttributeChangeCallback(request.path);
363-
CurrentContext().dataModelChangeListener->MarkDirty(request.path);
409+
// TODO: this is awkward since it provides AAI no control over this, specifically
410+
// AAI may not want to increase versions for some attributes that are Q
411+
emberAfAttributeChanged(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId, &change_listener);
364412
}
365413
return *aai_result;
366414
}
@@ -376,33 +424,28 @@ DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const Dat
376424
return Status::InvalidValue;
377425
}
378426

427+
EmberAfWriteDataInput dataInput(dataBuffer.data(), (*attributeMetadata)->attributeType);
428+
429+
dataInput.SetChangeListener(&change_listener);
430+
// TODO: dataInput.SetMarkDirty() should be according to `ChangesOmmited`
431+
379432
if (request.operationFlags.Has(DataModel::OperationFlags::kInternal))
380433
{
381434
// Internal requests use the non-External interface that has less enforcement
382435
// than the external version (e.g. does not check/enforce writable settings, does not
383436
// validate attribute types) - see attribute-table.h documentation for details.
384-
status = emberAfWriteAttribute(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId,
385-
dataBuffer.data(), (*attributeMetadata)->attributeType);
437+
status = emberAfWriteAttribute(request.path, dataInput);
386438
}
387439
else
388440
{
389-
status =
390-
emAfWriteAttributeExternal(request.path, EmberAfWriteDataInput(dataBuffer.data(), (*attributeMetadata)->attributeType));
441+
status = emAfWriteAttributeExternal(request.path, dataInput);
391442
}
392443

393444
if (status != Protocols::InteractionModel::Status::Success)
394445
{
395446
return status;
396447
}
397448

398-
// TODO: this WILL requre updates
399-
//
400-
// - Internal writes may need to be able to decide if to mark things dirty or not (see AAI as well)
401-
// - Changes-ommited paths should not be marked dirty (ember is not aware of that flag)
402-
// - This likely maps to `MatterReportingAttributeChangeCallback` HOWEVER current ember write functions
403-
// will selectively call that one depending on old attribute state (i.e. calling every time is a
404-
// change in behavior)
405-
CurrentContext().dataModelChangeListener->MarkDirty(request.path);
406449
return CHIP_NO_ERROR;
407450
}
408451

‎src/app/codegen-data-model-provider/tests/EmberReadWriteOverride.cpp

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
#include "EmberReadWriteOverride.h"
1818

19+
#include <app/AttributePathParams.h>
20+
#include <app/util/af-types.h>
1921
#include <app/util/attribute-storage.h>
2022
#include <app/util/attribute-table.h>
2123
#include <app/util/ember-io-storage.h>
@@ -116,9 +118,15 @@ Status emAfWriteAttributeExternal(const chip::app::ConcreteAttributePath & path,
116118
// copy over as much data as possible
117119
// NOTE: we do NOT use (*metadata)->size since it is unclear if our mocks set that correctly
118120
size_t len = std::min<size_t>(sizeof(gEmberIoBuffer), chip::app::Compatibility::Internal::gEmberAttributeIOBufferSpan.size());
121+
119122
memcpy(gEmberIoBuffer, input.dataPtr, len);
120123
gEmberIoBufferFill = len;
121124

125+
if (input.changeListener != nullptr)
126+
{
127+
input.changeListener->MarkDirty(chip::app::AttributePathParams(path.mEndpointId, path.mClusterId, path.mAttributeId));
128+
}
129+
122130
return Status::Success;
123131
}
124132

@@ -128,3 +136,8 @@ Status emberAfWriteAttribute(chip::EndpointId endpoint, chip::ClusterId cluster,
128136
return emAfWriteAttributeExternal(chip::app::ConcreteAttributePath(endpoint, cluster, attributeID),
129137
EmberAfWriteDataInput(dataPtr, dataType));
130138
}
139+
140+
Status emberAfWriteAttribute(const chip::app::ConcreteAttributePath & path, const EmberAfWriteDataInput & input)
141+
{
142+
return emAfWriteAttributeExternal(path, input);
143+
}

‎src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp

+46-10
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ bool operator==(const Access::SubjectDescriptor & a, const Access::SubjectDescri
132132
class TestProviderChangeListener : public ProviderChangeListener
133133
{
134134
public:
135-
void MarkDirty(const ConcreteAttributePath & path) override { mDirtyList.push_back(path); }
135+
void MarkDirty(const AttributePathParams & path) override { mDirtyList.push_back(path); }
136136

137-
std::vector<ConcreteAttributePath> & DirtyList() { return mDirtyList; }
138-
const std::vector<ConcreteAttributePath> & DirtyList() const { return mDirtyList; }
137+
std::vector<AttributePathParams> & DirtyList() { return mDirtyList; }
138+
const std::vector<AttributePathParams> & DirtyList() const { return mDirtyList; }
139139

140140
private:
141-
std::vector<ConcreteAttributePath> mDirtyList;
141+
std::vector<AttributePathParams> mDirtyList;
142142
};
143143

144144
class TestEventGenerator : public EventsGenerator
@@ -802,12 +802,32 @@ void TestEmberScalarTypeWrite(const typename NumericAttributeTraits<T>::WorkingT
802802

803803
EXPECT_EQ(actual, value);
804804
ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
805-
EXPECT_EQ(model.ChangeListener().DirtyList()[0], test.request.path);
805+
EXPECT_EQ(model.ChangeListener().DirtyList()[0],
806+
AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
806807

807808
// reset for the next test
808809
model.ChangeListener().DirtyList().clear();
809810
}
810811

812+
// nullable test: write null to make sure content of buffer changed (otherwise it will be a noop for dirty checking)
813+
{
814+
TestWriteRequest test(
815+
kAdminSubjectDescriptor,
816+
ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType)));
817+
818+
using NumericType = NumericAttributeTraits<T>;
819+
using NullableType = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
820+
AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());
821+
822+
// write should succeed
823+
ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR);
824+
825+
// dirty: we changed the value to null
826+
ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
827+
EXPECT_EQ(model.ChangeListener().DirtyList()[0],
828+
AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
829+
}
830+
811831
// nullable test
812832
{
813833
TestWriteRequest test(
@@ -827,8 +847,10 @@ void TestEmberScalarTypeWrite(const typename NumericAttributeTraits<T>::WorkingT
827847
typename NumericAttributeTraits<T>::WorkingType actual = NumericAttributeTraits<T>::StorageToWorking(storage);
828848

829849
ASSERT_EQ(actual, value);
830-
ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
831-
EXPECT_EQ(model.ChangeListener().DirtyList()[0], test.request.path);
850+
// dirty a 2nd time when we moved from null to a real value
851+
ASSERT_EQ(model.ChangeListener().DirtyList().size(), 2u);
852+
EXPECT_EQ(model.ChangeListener().DirtyList()[1],
853+
AttributePathParams(test.request.path.mEndpointId, test.request.path.mClusterId, test.request.path.mAttributeId));
832854
}
833855
}
834856

@@ -1994,7 +2016,20 @@ TEST(TestCodegenModelViaMocks, EmberAttributeWriteAclDeny)
19942016
CodegenDataModelProviderWithContext model;
19952017
ScopedMockAccessControl accessControl;
19962018

1997-
TestWriteRequest test(kDenySubjectDescriptor, ConcreteDataAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
2019+
/* Using this path is also failing existence checks, so this cannot be enabled
2020+
* until we fix ordering of ACL to be done before existence checks
2021+
2022+
TestWriteRequest test(kDenySubjectDescriptor,
2023+
ConcreteDataAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10)));
2024+
AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
2025+
2026+
ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedAccess);
2027+
ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
2028+
*/
2029+
2030+
TestWriteRequest test(kDenySubjectDescriptor,
2031+
ConcreteDataAttributePath(kMockEndpoint3, MockClusterId(4),
2032+
MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE)));
19982033
AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
19992034

20002035
ASSERT_EQ(model.WriteAttribute(test.request, decoder), Status::UnsupportedAccess);
@@ -2431,12 +2466,13 @@ TEST(TestCodegenModelViaMocks, EmberWriteAttributeAccessInterfaceTest)
24312466

24322467
// AAI marks dirty paths
24332468
ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
2434-
EXPECT_EQ(model.ChangeListener().DirtyList()[0], kStructPath);
2469+
EXPECT_EQ(model.ChangeListener().DirtyList()[0],
2470+
AttributePathParams(kStructPath.mEndpointId, kStructPath.mClusterId, kStructPath.mAttributeId));
24352471

24362472
// AAI does not prevent read/write of regular attributes
24372473
// validate that once AAI is added, we still can go through writing regular bits (i.e.
24382474
// AAI returning "unknown" has fallback to ember)
2439-
TestEmberScalarTypeWrite<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>(1234);
2475+
TestEmberScalarTypeWrite<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>(4321);
24402476
TestEmberScalarNullWrite<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>();
24412477
}
24422478

‎src/app/data-model-provider/OperationTypes.h

+12-3
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,24 @@ struct ReadAttributeRequest : OperationRequest
7070

7171
enum class WriteFlags : uint32_t
7272
{
73-
kTimed = 0x0001, // Write is a timed write (i.e. a Timed Request Action preceeded it)
74-
kListBegin = 0x0002, // This is the FIRST list of data elements
75-
kListEnd = 0x0004, // This is the LAST list element to write
73+
kTimed = 0x0001, // Write is a timed write (i.e. a Timed Request Action preceeded it)
7674
};
7775

7876
struct WriteAttributeRequest : OperationRequest
7977
{
8078
ConcreteDataAttributePath path; // NOTE: this also contains LIST operation options (i.e. "data" path type)
8179
BitFlags<WriteFlags> writeFlags;
80+
81+
// The path of the previous successful write in the same write transaction, if any.
82+
//
83+
// In particular this means that a write to this path has succeeded before (i.e. it passed required ACL checks).
84+
// The intent for this is to allow short-cutting ACL checks when ACL is in progress of being updated:
85+
// - During write chunking, list writes can be of the form "reset list" followed by "append item by item"
86+
// - When ACL is updating, a reset to empty would result in the entire ACL being deny and the "append"
87+
// would fail.
88+
// callers are expected to keep track of a `previousSuccessPath` whenever a write succeeds (otherwise ACL
89+
// checks may fail)
90+
std::optional<ConcreteAttributePath> previousSuccessPath;
8291
};
8392

8493
enum class InvokeFlags : uint32_t

‎src/app/data-model-provider/ProviderChangeListener.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
#pragma once
1818

19-
#include <app/ConcreteAttributePath.h>
19+
#include <app/AttributePathParams.h>
2020

2121
namespace chip {
2222
namespace app {
@@ -39,7 +39,7 @@ class ProviderChangeListener
3939
/// Mark all attributes matching the given path (which may be a wildcard) dirty.
4040
///
4141
/// Wildcards are supported.
42-
virtual void MarkDirty(const ConcreteAttributePath & path) = 0;
42+
virtual void MarkDirty(const AttributePathParams & path) = 0;
4343
};
4444

4545
} // namespace DataModel

‎src/app/reporting/Engine.cpp

+11-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <app/RequiredPrivilege.h>
3434
#include <app/reporting/Engine.h>
3535
#include <app/reporting/Read.h>
36+
#include <app/reporting/reporting.h>
3637
#include <app/util/MatterCallbacks.h>
3738
#include <app/util/ember-compatibility-functions.h>
3839

@@ -1010,7 +1011,16 @@ void Engine::ScheduleUrgentEventDeliverySync(Optional<FabricIndex> fabricIndex)
10101011
Run();
10111012
}
10121013

1013-
}; // namespace reporting
1014+
void Engine::MarkDirty(const AttributePathParams & path)
1015+
{
1016+
CHIP_ERROR err = SetDirty(path);
1017+
if (err != CHIP_NO_ERROR)
1018+
{
1019+
ChipLogError(DataManagement, "Failed to set path dirty: %" CHIP_ERROR_FORMAT, err.Format());
1020+
}
1021+
}
1022+
1023+
} // namespace reporting
10141024
} // namespace app
10151025
} // namespace chip
10161026

‎src/app/reporting/Engine.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <access/AccessControl.h>
2828
#include <app/MessageDef/ReportDataMessage.h>
2929
#include <app/ReadHandler.h>
30+
#include <app/data-model-provider/ProviderChangeListener.h>
3031
#include <app/util/basic-types.h>
3132
#include <lib/core/CHIPCore.h>
3233
#include <lib/support/CodeUtils.h>
@@ -54,7 +55,7 @@ namespace reporting {
5455
* At its core, it tries to gather and pack as much relevant attributes changes and/or events as possible into a report
5556
* message before sending that to the reader. It continues to do so until it has no more work to do.
5657
*/
57-
class Engine
58+
class Engine : public DataModel::ProviderChangeListener
5859
{
5960
public:
6061
/**
@@ -140,6 +141,9 @@ class Engine
140141
size_t GetGlobalDirtySetSize() { return mGlobalDirtySet.Allocated(); }
141142
#endif
142143

144+
/* ProviderChangeListener implementation */
145+
void MarkDirty(const AttributePathParams & path) override;
146+
143147
private:
144148
/**
145149
* Main work-horse function that executes the run-loop.

‎src/app/tests/TestWriteInteraction.cpp

+8-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
19-
#include <memory>
2018
#include <utility>
2119

2220
#include <app-common/zap-generated/cluster-objects.h>
@@ -70,9 +68,12 @@ class TestWriteInteraction : public chip::Test::AppContext
7068
chip::MutableByteSpan span(buf);
7169
ASSERT_EQ(GetBobFabric()->GetCompressedFabricIdBytes(span), CHIP_NO_ERROR);
7270
ASSERT_EQ(chip::GroupTesting::InitData(&gGroupsProvider, GetBobFabricIndex(), span), CHIP_NO_ERROR);
71+
72+
mOldProvider = InteractionModelEngine::GetInstance()->SetDataModelProvider(&TestImCustomDataModel::Instance());
7373
}
7474
void TearDown() override
7575
{
76+
InteractionModelEngine::GetInstance()->SetDataModelProvider(mOldProvider);
7677
chip::Credentials::GroupDataProvider * provider = chip::Credentials::GetGroupDataProvider();
7778
if (provider != nullptr)
7879
{
@@ -93,6 +94,9 @@ class TestWriteInteraction : public chip::Test::AppContext
9394
static void AddAttributeStatus(WriteHandler & aWriteHandler);
9495
static void GenerateWriteRequest(bool aIsTimedWrite, System::PacketBufferHandle & aPayload);
9596
static void GenerateWriteResponse(System::PacketBufferHandle & aPayload);
97+
98+
private:
99+
chip::app::DataModel::Provider * mOldProvider = nullptr;
96100
};
97101

98102
class TestExchangeDelegate : public Messaging::ExchangeDelegate
@@ -296,7 +300,8 @@ TEST_F(TestWriteInteraction, TestWriteHandler)
296300

297301
System::PacketBufferHandle buf = System::PacketBufferHandle::New(System::PacketBuffer::kMaxSize);
298302

299-
writeHandler.Init(chip::app::InteractionModelEngine::GetInstance());
303+
writeHandler.Init(chip::app::InteractionModelEngine::GetInstance()->GetDataModelProvider(),
304+
chip::app::InteractionModelEngine::GetInstance());
300305

301306
GenerateWriteRequest(messageIsTimed, buf);
302307

‎src/app/tests/test-interaction-model-api.cpp

+27-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ class TestOnlyAttributeValueEncoderAccessor
4646
AttributeValueEncoder & mEncoder;
4747
};
4848

49+
class TestOnlyAttributeValueDecoderAccessor
50+
{
51+
public:
52+
TestOnlyAttributeValueDecoderAccessor(AttributeValueDecoder & decoder) : mDecoder(decoder) {}
53+
54+
TLV::TLVReader & GetTlvReader() { return mDecoder.mReader; }
55+
void SetTriedDecode(bool triedDecode) { mDecoder.mTriedDecode = triedDecode; }
56+
57+
private:
58+
AttributeValueDecoder & mDecoder;
59+
};
60+
4961
// Used by the code in TestWriteInteraction.cpp (and generally tests that interact with the WriteHandler may need this).
5062
const EmberAfAttributeMetadata * GetAttributeMetadata(const ConcreteAttributePath & aConcreteClusterPath)
5163
{
@@ -170,7 +182,21 @@ ActionReturnStatus TestImCustomDataModel::ReadAttribute(const ReadAttributeReque
170182

171183
ActionReturnStatus TestImCustomDataModel::WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder)
172184
{
173-
return CHIP_ERROR_NOT_IMPLEMENTED;
185+
if (request.path.mDataVersion.HasValue() && request.path.mDataVersion.Value() == Test::kRejectedDataVersion)
186+
{
187+
return CHIP_IM_GLOBAL_STATUS(DataVersionMismatch);
188+
}
189+
190+
TestOnlyAttributeValueDecoderAccessor decodeAccess(decoder);
191+
192+
decodeAccess.SetTriedDecode(true);
193+
194+
TLV::TLVWriter writer;
195+
writer.Init(chip::Test::attributeDataTLV);
196+
writer.CopyElement(TLV::AnonymousTag(), decodeAccess.GetTlvReader());
197+
chip::Test::attributeDataTLVLen = writer.GetLengthWritten();
198+
199+
return CHIP_NO_ERROR;
174200
}
175201

176202
std::optional<ActionReturnStatus> TestImCustomDataModel::Invoke(const InvokeRequest & request,

‎src/app/util/mock/CodegenEmberMocks.cpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717
#include <app/reporting/reporting.h>
18+
#include <app/util/af-types.h>
1819
#include <app/util/attribute-storage.h>
1920
#include <app/util/attribute-table.h>
2021

@@ -41,8 +42,13 @@ Status emAfWriteAttributeExternal(const chip::app::ConcreteAttributePath & path,
4142
}
4243

4344
Status emberAfWriteAttribute(chip::EndpointId endpoint, chip::ClusterId cluster, chip::AttributeId attributeID, uint8_t * dataPtr,
44-
EmberAfAttributeType dataType)
45+
EmberAfAttributeType dataType, chip::app::MarkAttributeDirty markDirty)
4546
{
4647
return emAfWriteAttributeExternal(chip::app::ConcreteAttributePath(endpoint, cluster, attributeID),
4748
EmberAfWriteDataInput(dataPtr, dataType));
4849
}
50+
51+
Status emberAfWriteAttribute(const chip::app::ConcreteAttributePath & path, const EmberAfWriteDataInput & input)
52+
{
53+
return emAfWriteAttributeExternal(path, input);
54+
}

‎src/app/util/mock/attribute-storage.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ DataVersion * emberAfDataVersionStorage(const chip::app::ConcreteClusterPath & a
313313
return &dataVersion;
314314
}
315315

316+
void emberAfAttributeChanged(EndpointId endpoint, ClusterId clusterId, AttributeId attributeId,
317+
AttributesChangedListener * listener)
318+
{
319+
dataVersion++;
320+
listener->MarkDirty(AttributePathParams(endpoint, clusterId, attributeId));
321+
}
322+
316323
namespace chip {
317324
namespace app {
318325

‎src/controller/tests/data_model/DataModelFixtures.cpp

+139-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ class TestOnlyAttributeValueEncoderAccessor
5050
AttributeValueEncoder & mEncoder;
5151
};
5252

53+
class TestOnlyAttributeValueDecoderAccessor
54+
{
55+
public:
56+
TestOnlyAttributeValueDecoderAccessor(AttributeValueDecoder & decoder) : mDecoder(decoder) {}
57+
58+
TLV::TLVReader & GetTlvReader() { return mDecoder.mReader; }
59+
60+
private:
61+
AttributeValueDecoder & mDecoder;
62+
};
63+
5364
namespace DataModelTests {
5465

5566
ScopedChangeOnly<ReadResponseDirective> gReadResponseDirective(ReadResponseDirective::kSendDataResponse);
@@ -300,7 +311,8 @@ CHIP_ERROR WriteSingleClusterData(const Access::SubjectDescriptor & aSubjectDesc
300311
}
301312
if (aPath.mClusterId == Clusters::UnitTesting::Id && aPath.mAttributeId == Attributes::ListFabricScoped::Id)
302313
{
303-
// Mock a invalid SubjectDescriptor
314+
// Mock an invalid SubjectDescriptor.
315+
// NOTE: completely ignores the passed-in subjectDescriptor
304316
AttributeValueDecoder decoder(aReader, Access::SubjectDescriptor());
305317
if (!aPath.IsListOperation() || aPath.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
306318
{
@@ -522,7 +534,132 @@ ActionReturnStatus CustomDataModel::ReadAttribute(const ReadAttributeRequest & r
522534

523535
ActionReturnStatus CustomDataModel::WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder)
524536
{
525-
return CHIP_ERROR_NOT_IMPLEMENTED;
537+
static ListIndex listStructOctetStringElementCount = 0;
538+
539+
if (request.path.mDataVersion.HasValue() && request.path.mDataVersion.Value() == kRejectedDataVersion)
540+
{
541+
return InteractionModel::Status::DataVersionMismatch;
542+
}
543+
544+
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
545+
request.path.mAttributeId == Attributes::ListStructOctetString::TypeInfo::GetAttributeId())
546+
{
547+
if (gWriteResponseDirective == WriteResponseDirective::kSendAttributeSuccess)
548+
{
549+
if (!request.path.IsListOperation() || request.path.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
550+
{
551+
552+
Attributes::ListStructOctetString::TypeInfo::DecodableType value;
553+
554+
ReturnErrorOnFailure(decoder.Decode(value));
555+
556+
auto iter = value.begin();
557+
listStructOctetStringElementCount = 0;
558+
while (iter.Next())
559+
{
560+
auto & item = iter.GetValue();
561+
562+
VerifyOrReturnError(item.member1 == listStructOctetStringElementCount, CHIP_ERROR_INVALID_ARGUMENT);
563+
listStructOctetStringElementCount++;
564+
}
565+
return CHIP_NO_ERROR;
566+
}
567+
568+
if (request.path.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem)
569+
{
570+
Structs::TestListStructOctet::DecodableType item;
571+
ReturnErrorOnFailure(decoder.Decode(item));
572+
VerifyOrReturnError(item.member1 == listStructOctetStringElementCount, CHIP_ERROR_INVALID_ARGUMENT);
573+
listStructOctetStringElementCount++;
574+
575+
return CHIP_NO_ERROR;
576+
}
577+
578+
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
579+
}
580+
581+
return CHIP_IM_GLOBAL_STATUS(Failure);
582+
}
583+
if (request.path.mClusterId == Clusters::UnitTesting::Id && request.path.mAttributeId == Attributes::ListFabricScoped::Id)
584+
{
585+
// TODO(backwards compatibility): unit tests here undoes the subject descriptor usage
586+
// - original tests were completely bypassing the passed in subject descriptor for this test
587+
// and overriding it with a invalid subject descriptor
588+
// - we do the same here, however this seems somewhat off: decoder.Decode() will fail for list
589+
// items so we could just return the error directly without this extra step
590+
591+
// Mock an invalid Subject Descriptor
592+
AttributeValueDecoder invalidSubjectDescriptorDecoder(TestOnlyAttributeValueDecoderAccessor(decoder).GetTlvReader(),
593+
Access::SubjectDescriptor());
594+
if (!request.path.IsListOperation() || request.path.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
595+
{
596+
Attributes::ListFabricScoped::TypeInfo::DecodableType value;
597+
598+
ReturnErrorOnFailure(invalidSubjectDescriptorDecoder.Decode(value));
599+
600+
auto iter = value.begin();
601+
while (iter.Next())
602+
{
603+
auto & item = iter.GetValue();
604+
(void) item;
605+
}
606+
}
607+
else if (request.path.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem)
608+
{
609+
Structs::TestFabricScoped::DecodableType item;
610+
ReturnErrorOnFailure(invalidSubjectDescriptorDecoder.Decode(item));
611+
}
612+
else
613+
{
614+
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
615+
}
616+
return CHIP_NO_ERROR;
617+
}
618+
619+
// Boolean attribute of unit testing cluster triggers "multiple errors" case.
620+
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
621+
request.path.mAttributeId == Attributes::Boolean::TypeInfo::GetAttributeId())
622+
{
623+
// TODO(IMDM): this used to send 4 responses (hence the multiple status)
624+
//
625+
// for (size_t i = 0; i < 4; ++i)
626+
// {
627+
// aWriteHandler->AddStatus(request.path, status);
628+
// }
629+
//
630+
// which are NOT encodable by a simple response. It is unclear how this is
631+
// convertible (if at all): we write path by path only. Having multiple
632+
// responses for the same path within the write code makes no sense
633+
//
634+
// This should NOT be possible anymore when one can only return a single
635+
// status (nobody has access to multiple path status updates at this level)
636+
switch (gWriteResponseDirective)
637+
{
638+
case WriteResponseDirective::kSendMultipleSuccess:
639+
return InteractionModel::Status::Success;
640+
case WriteResponseDirective::kSendMultipleErrors:
641+
return InteractionModel::Status::Failure;
642+
default:
643+
chipDie();
644+
}
645+
}
646+
647+
if (request.path.mClusterId == Clusters::UnitTesting::Id &&
648+
request.path.mAttributeId == Attributes::Int8u::TypeInfo::GetAttributeId())
649+
{
650+
switch (gWriteResponseDirective)
651+
{
652+
case WriteResponseDirective::kSendClusterSpecificSuccess:
653+
return InteractionModel::ClusterStatusCode::ClusterSpecificSuccess(kExampleClusterSpecificSuccess);
654+
case WriteResponseDirective::kSendClusterSpecificFailure:
655+
return InteractionModel::ClusterStatusCode::ClusterSpecificFailure(kExampleClusterSpecificFailure);
656+
default:
657+
// this should not be reached, our tests only set up these for this test case
658+
chipDie();
659+
}
660+
}
661+
662+
return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
526663
}
527664

528665
std::optional<ActionReturnStatus> CustomDataModel::Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments,

‎src/controller/tests/data_model/TestWrite.cpp

+16-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ class SingleWriteCallback : public WriteClient::Callback
8585
class TestWrite : public chip::Test::AppContext
8686
{
8787
public:
88+
void SetUp() override
89+
{
90+
chip::Test::AppContext::SetUp();
91+
mOldProvider = InteractionModelEngine::GetInstance()->SetDataModelProvider(&CustomDataModel::Instance());
92+
}
93+
94+
// Performs teardown for each individual test in the test suite
95+
void TearDown() override
96+
{
97+
InteractionModelEngine::GetInstance()->SetDataModelProvider(mOldProvider);
98+
chip::Test::AppContext::TearDown();
99+
}
100+
88101
void ResetCallback() { mSingleWriteCallback.reset(); }
89102

90103
void PrepareWriteCallback(ConcreteAttributePath path) { mSingleWriteCallback = std::make_unique<SingleWriteCallback>(path); }
@@ -93,6 +106,7 @@ class TestWrite : public chip::Test::AppContext
93106

94107
protected:
95108
std::unique_ptr<SingleWriteCallback> mSingleWriteCallback;
109+
chip::app::DataModel::Provider * mOldProvider = nullptr;
96110
};
97111

98112
TEST_F(TestWrite, TestDataResponse)
@@ -128,7 +142,8 @@ TEST_F(TestWrite, TestDataResponse)
128142

129143
DrainAndServiceIO();
130144

131-
EXPECT_TRUE(onSuccessCbInvoked && !onFailureCbInvoked);
145+
EXPECT_TRUE(onSuccessCbInvoked);
146+
EXPECT_FALSE(onFailureCbInvoked);
132147
EXPECT_EQ(chip::app::InteractionModelEngine::GetInstance()->GetNumActiveWriteHandlers(), 0u);
133148
EXPECT_EQ(GetExchangeManager().GetNumActiveExchanges(), 0u);
134149
}

0 commit comments

Comments
 (0)
Please sign in to comment.