blob: 6e10fae93cf58adff15a5a48baac29d71652fd9e [file] [log] [blame]
/*
* Copyright (c) 2024 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <app/reporting/Read-Checked.h>
#include <app/data-model-provider/ActionReturnStatus.h>
#include <app/reporting/Read-DataModel.h>
#include <app/reporting/Read-Ember.h>
#include <app/util/MatterCallbacks.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/StringBuilder.h>
namespace chip {
namespace app {
namespace reporting {
namespace CheckedImpl {
namespace {
using DataModel::ActionReturnStatus;
/// Checkpoints and saves the state (including error state) for a
/// AttributeReportIBs::Builder
class ScopedAttributeReportIBsBuilderState
{
public:
ScopedAttributeReportIBsBuilderState(AttributeReportIBs::Builder & builder) : mBuilder(builder), mError(mBuilder.GetError())
{
mBuilder.Checkpoint(mCheckpoint);
}
~ScopedAttributeReportIBsBuilderState()
{
mBuilder.Rollback(mCheckpoint);
mBuilder.ResetError(mError);
}
private:
AttributeReportIBs::Builder & mBuilder;
chip::TLV::TLVWriter mCheckpoint;
CHIP_ERROR mError;
};
} // namespace
ActionReturnStatus RetrieveClusterData(DataModel::Provider * dataModel, const Access::SubjectDescriptor & subjectDescriptor,
bool isFabricFiltered, AttributeReportIBs::Builder & reportBuilder,
const ConcreteReadAttributePath & path, AttributeEncodeState * encoderState)
{
ChipLogDetail(DataManagement, "<RE:Run> Cluster %" PRIx32 ", Attribute %" PRIx32 " is dirty", path.mClusterId,
path.mAttributeId);
DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Read,
DataModelCallbacks::OperationOrder::Pre, path);
ActionReturnStatus statusEmber(CHIP_NO_ERROR);
uint32_t lengthWrittenEmber = 0;
// a copy for DM logic only. Ember changes state directly
// IMPORTANT: the copy MUST be taken BEFORE ember processes/changes encoderState inline.
AttributeEncodeState stateDm(encoderState);
{
ScopedAttributeReportIBsBuilderState builderState(reportBuilder); // temporary only
statusEmber =
EmberImpl::RetrieveClusterData(dataModel, subjectDescriptor, isFabricFiltered, reportBuilder, path, encoderState);
lengthWrittenEmber = reportBuilder.GetWriter()->GetLengthWritten();
}
ActionReturnStatus statusDm = DataModelImpl::RetrieveClusterData(dataModel, subjectDescriptor, isFabricFiltered, reportBuilder,
path, encoderState != nullptr ? &stateDm : nullptr);
if (statusEmber != statusDm)
{
ActionReturnStatus::StringStorage buffer;
// Note log + chipDie instead of VerifyOrDie so that breakpoints (and usage of rr)
// is easier to debug.
ChipLogError(Test, "Different return codes between ember and DM");
ChipLogError(Test, " Ember status: %s", statusEmber.c_str(buffer));
ChipLogError(Test, " DM status: %s", statusDm.c_str(buffer));
// For time-dependent data, we may have size differences here: one data fitting in buffer
// while another not, resulting in different errors (success vs out of space).
//
// Make unit tests strict; otherwise allow it with potentially odd mismatch errors
// (in which case logs will be odd, however we also expect Checked versions to only
// run for a short period until we switch over to either ember or DM completely).
#if CHIP_CONFIG_DATA_MODEL_CHECK_DIE_ON_FAILURE
chipDie();
#endif
}
// data should be identical for most cases EXCEPT that for time-deltas (e.g. seconds since boot or similar)
// it may actually differ. As a result, the amount of data written in bytes MUST be the same, however if the rest of the
// data is not the same, we just print it out as a warning for manual inspection
//
// We have no direct access to TLV buffer data (especially given backing store splits)
// so for now we check that data length was identical.
//
// NOTE: RetrieveClusterData is responsible for encoding StatusIB errors in case of failures
// so we validate length written requirements for BOTH success and failure.
//
// NOTE: data length is NOT reliable if the data content differs in encoding length. E.g. numbers changing
// from 0xFF to 0x100 or similar will use up more space.
// For unit tests we make the validation strict, however for runtime we just report an
// error for different sizes.
if (lengthWrittenEmber != reportBuilder.GetWriter()->GetLengthWritten())
{
ChipLogError(Test, "Different written length: %" PRIu32 " (Ember) vs %" PRIu32 " (DataModel)", lengthWrittenEmber,
reportBuilder.GetWriter()->GetLengthWritten());
#if CHIP_CONFIG_DATA_MODEL_CHECK_DIE_ON_FAILURE
chipDie();
#endif
}
// For chunked reads, the encoder state MUST be identical (since this is what controls
// where chunking resumes).
if (statusEmber.IsOutOfSpaceEncodingResponse())
{
// Encoder state MUST match on partial reads (used by chunking)
// specifically ReadViaAccessInterface in ember-compatibility-functions only
// sets the encoder state in case an error occurs.
if (encoderState != nullptr)
{
if (encoderState->AllowPartialData() != stateDm.AllowPartialData())
{
ChipLogError(Test, "Different partial data");
// NOTE: die on unit tests only, since partial data size may differ across
// time-dependent data (very rarely because fast code, but still possible)
#if CHIP_CONFIG_DATA_MODEL_CHECK_DIE_ON_FAILURE
chipDie();
#endif
}
if (encoderState->CurrentEncodingListIndex() != stateDm.CurrentEncodingListIndex())
{
ChipLogError(Test, "Different partial data");
// NOTE: die on unit tests only, since partial data size may differ across
// time-dependent data (very rarely because fast code, but still possible)
#if CHIP_CONFIG_DATA_MODEL_CHECK_DIE_ON_FAILURE
chipDie();
#endif
}
}
}
DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Read,
DataModelCallbacks::OperationOrder::Post, path);
return statusDm;
}
bool IsClusterDataVersionEqualTo(DataModel::Provider * dataModel, const ConcreteClusterPath & path, DataVersion dataVersion)
{
bool emberResult = EmberImpl::IsClusterDataVersionEqualTo(dataModel, path, dataVersion);
bool dmResult = DataModelImpl::IsClusterDataVersionEqualTo(dataModel, path, dataVersion);
if (emberResult != dmResult)
{
ChipLogError(Test, "Different data model check result between ember (%s) and data model provider(%s)",
emberResult ? "TRUE" : "FALSE", dmResult ? "TRUE" : "FALSE");
#if CHIP_CONFIG_DATA_MODEL_CHECK_DIE_ON_FAILURE
chipDie();
#endif
}
return dmResult;
}
} // namespace CheckedImpl
} // namespace reporting
} // namespace app
} // namespace chip