|
41 | 41 | namespace chip { |
42 | 42 | namespace Test { |
43 | 43 |
|
44 | | - // Helper class for testing clusters. |
45 | | - // |
46 | | - // This class ensures that data read by attribute is referencing valid memory for all |
47 | | - // read requests until the ClusterTester object goes out of scope. (for the case where the underlying read references a list or |
48 | | - // string that points to TLV data). |
49 | | - // |
50 | | - // Read/Write of all attribute types should work, but make sure to use ::Type for encoding |
51 | | - // and ::DecodableType for decoding structure types. |
52 | | - // |
53 | | - // Example of usage: |
54 | | - // |
55 | | - // ExampleCluster cluster(someEndpointId); |
56 | | - // |
57 | | - // // Possibly steps to setup the cluster |
58 | | - // |
59 | | - // ClusterTester tester(cluster); |
60 | | - // app::Clusters::ExampleCluster::Attributes::FeatureMap::TypeInfo::DecodableType features; |
61 | | - // ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, features), CHIP_NO_ERROR); |
62 | | - // |
63 | | - // app::Clusters::ExampleCluster::Attributes::ExampleListAttribute::TypeInfo::DecodableType list; |
64 | | - // ASSERT_EQ(tester.ReadAttribute(LabelList::Id, list), CHIP_NO_ERROR); |
65 | | - // auto it = list.begin(); |
66 | | - // while (it.Next()) |
67 | | - // { |
68 | | - // ASSERT_GT(it.GetValue().label.size(), 0u); |
69 | | - // } |
70 | | - // |
71 | | - class ClusterTester { |
72 | | - public: |
73 | | - ClusterTester(app::ServerClusterInterface & cluster) |
74 | | - : mCluster(cluster) |
75 | | - { |
76 | | - } |
| 44 | +// Helper class for testing clusters. |
| 45 | +// |
| 46 | +// This class ensures that data read by attribute is referencing valid memory for all |
| 47 | +// read requests until the ClusterTester object goes out of scope. (for the case where the underlying read references a list or |
| 48 | +// string that points to TLV data). |
| 49 | +// |
| 50 | +// Read/Write of all attribute types should work, but make sure to use ::Type for encoding |
| 51 | +// and ::DecodableType for decoding structure types. |
| 52 | +// |
| 53 | +// Example of usage: |
| 54 | +// |
| 55 | +// ExampleCluster cluster(someEndpointId); |
| 56 | +// |
| 57 | +// // Possibly steps to setup the cluster |
| 58 | +// |
| 59 | +// ClusterTester tester(cluster); |
| 60 | +// app::Clusters::ExampleCluster::Attributes::FeatureMap::TypeInfo::DecodableType features; |
| 61 | +// ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, features), CHIP_NO_ERROR); |
| 62 | +// |
| 63 | +// app::Clusters::ExampleCluster::Attributes::ExampleListAttribute::TypeInfo::DecodableType list; |
| 64 | +// ASSERT_EQ(tester.ReadAttribute(LabelList::Id, list), CHIP_NO_ERROR); |
| 65 | +// auto it = list.begin(); |
| 66 | +// while (it.Next()) |
| 67 | +// { |
| 68 | +// ASSERT_GT(it.GetValue().label.size(), 0u); |
| 69 | +// } |
| 70 | +// |
| 71 | +class ClusterTester |
| 72 | +{ |
| 73 | +public: |
| 74 | + ClusterTester(app::ServerClusterInterface & cluster) : mCluster(cluster) {} |
| 75 | + |
| 76 | + app::ServerClusterContext & GetServerClusterContext() { return mTestServerClusterContext.Get(); } |
| 77 | + |
| 78 | + // Read attribute into `out` parameter. |
| 79 | + // The `out` parameter must be of the correct type for the attribute being read. |
| 80 | + // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::DecodableType` for the `out` parameter to be spec |
| 81 | + // compliant (see the comment of the class for usage example). |
| 82 | + // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster. |
| 83 | + // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
| 84 | + template <typename T> |
| 85 | + app::DataModel::ActionReturnStatus ReadAttribute(AttributeId attr_id, T & out) |
| 86 | + { |
| 87 | + VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE); |
| 88 | + auto path = mCluster.GetPaths()[0]; |
| 89 | + |
| 90 | + // Store the read operation in a vector<std::unique_ptr<...>> to ensure its lifetime |
| 91 | + // using std::unique_ptr because ReadOperation is non-copyable and non-movable |
| 92 | + // vector reallocation is not an issue since we store unique_ptrs |
| 93 | + std::unique_ptr<app::Testing::ReadOperation> readOperation = |
| 94 | + std::make_unique<app::Testing::ReadOperation>(path.mEndpointId, path.mClusterId, attr_id); |
| 95 | + |
| 96 | + mReadOperations.push_back(std::move(readOperation)); |
| 97 | + app::Testing::ReadOperation & readOperationRef = *mReadOperations.back().get(); |
| 98 | + |
| 99 | + std::unique_ptr<app::AttributeValueEncoder> encoder = readOperationRef.StartEncoding(); |
| 100 | + app::DataModel::ActionReturnStatus status = mCluster.ReadAttribute(readOperationRef.GetRequest(), *encoder); |
| 101 | + VerifyOrReturnError(status.IsSuccess(), status); |
| 102 | + ReturnErrorOnFailure(readOperationRef.FinishEncoding()); |
| 103 | + |
| 104 | + std::vector<app::Testing::DecodedAttributeData> attributeData; |
| 105 | + ReturnErrorOnFailure(readOperationRef.GetEncodedIBs().Decode(attributeData)); |
| 106 | + VerifyOrReturnError(attributeData.size() == 1u, CHIP_ERROR_INCORRECT_STATE); |
| 107 | + |
| 108 | + return app::DataModel::Decode(attributeData[0].dataReader, out); |
| 109 | + } |
| 110 | + |
| 111 | + // Write attribute from `value` parameter. |
| 112 | + // The `value` parameter must be of the correct type for the attribute being written. |
| 113 | + // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::Type` for the `value` parameter to be spec |
| 114 | + // compliant (see the comment of the class for usage example). |
| 115 | + // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster. |
| 116 | + // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
| 117 | + template <typename T> |
| 118 | + app::DataModel::ActionReturnStatus WriteAttribute(AttributeId attr_id, const T & value) |
| 119 | + { |
| 120 | + VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE); |
| 121 | + auto path = mCluster.GetPaths()[0]; |
| 122 | + |
| 123 | + app::Testing::WriteOperation writeOperation(path.mEndpointId, path.mClusterId, attr_id); |
| 124 | + |
| 125 | + app::AttributeValueDecoder decoder = writeOperation.DecoderFor(value); |
| 126 | + return mCluster.WriteAttribute(writeOperation.GetRequest(), decoder); |
| 127 | + } |
| 128 | + |
| 129 | + // Result structure for Invoke operations, containing both status and decoded response. |
| 130 | + template <typename ResponseType> |
| 131 | + struct InvokeResult |
| 132 | + { |
| 133 | + std::optional<app::DataModel::ActionReturnStatus> status; |
| 134 | + std::optional<ResponseType> response; |
| 135 | + |
| 136 | + // Returns true if the command was successful and response is available |
| 137 | + bool IsSuccess() const { return status.has_value() && status->IsSuccess() && response.has_value(); } |
| 138 | + }; |
77 | 139 |
|
78 | | - app::ServerClusterContext & GetServerClusterContext() { return mTestServerClusterContext.Get(); } |
| 140 | + // Invoke a command and return the decoded result. |
| 141 | + // The `RequestType`, `ResponseType` type-parameters must be of the correct type for the command being invoked. |
| 142 | + // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type` for the `RequestType` type-parameter to be spec compliant |
| 143 | + // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type::ResponseType` for the `ResponseType` type-parameter to be |
| 144 | + // spec compliant Will construct the command path using the first path returned by `GetPaths()` on the cluster. |
| 145 | + // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
| 146 | + template <typename ResponseType, typename RequestType> |
| 147 | + [[nodiscard]] InvokeResult<ResponseType> Invoke(chip::CommandId commandId, const RequestType & request) |
| 148 | + { |
| 149 | + InvokeResult<ResponseType> result; |
79 | 150 |
|
80 | | - // Read attribute into `out` parameter. |
81 | | - // The `out` parameter must be of the correct type for the attribute being read. |
82 | | - // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::DecodableType` for the `out` parameter to be spec |
83 | | - // compliant (see the comment of the class for usage example). |
84 | | - // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster. |
85 | | - // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
86 | | - template <typename T> |
87 | | - app::DataModel::ActionReturnStatus ReadAttribute(AttributeId attr_id, T & out) |
88 | | - { |
89 | | - VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE); |
90 | | - auto path = mCluster.GetPaths()[0]; |
| 151 | + const auto & paths = mCluster.GetPaths(); |
| 152 | + VerifyOrReturnValue(paths.size() == 1u, result); |
91 | 153 |
|
92 | | - // Store the read operation in a vector<std::unique_ptr<...>> to ensure its lifetime |
93 | | - // using std::unique_ptr because ReadOperation is non-copyable and non-movable |
94 | | - // vector reallocation is not an issue since we store unique_ptrs |
95 | | - std::unique_ptr<app::Testing::ReadOperation> readOperation = std::make_unique<app::Testing::ReadOperation>(path.mEndpointId, path.mClusterId, attr_id); |
| 154 | + mHandler.ClearResponses(); |
| 155 | + mHandler.ClearStatuses(); |
96 | 156 |
|
97 | | - mReadOperations.push_back(std::move(readOperation)); |
98 | | - app::Testing::ReadOperation & readOperationRef = *mReadOperations.back().get(); |
| 157 | + const app::DataModel::InvokeRequest invokeRequest = { .path = { paths[0].mEndpointId, paths[0].mClusterId, commandId } }; |
99 | 158 |
|
100 | | - std::unique_ptr<app::AttributeValueEncoder> encoder = readOperationRef.StartEncoding(); |
101 | | - app::DataModel::ActionReturnStatus status = mCluster.ReadAttribute(readOperationRef.GetRequest(), *encoder); |
102 | | - VerifyOrReturnError(status.IsSuccess(), status); |
103 | | - ReturnErrorOnFailure(readOperationRef.FinishEncoding()); |
| 159 | + TLV::TLVWriter writer; |
| 160 | + writer.Init(mTlvBuffer); |
104 | 161 |
|
105 | | - std::vector<app::Testing::DecodedAttributeData> attributeData; |
106 | | - ReturnErrorOnFailure(readOperationRef.GetEncodedIBs().Decode(attributeData)); |
107 | | - VerifyOrReturnError(attributeData.size() == 1u, CHIP_ERROR_INCORRECT_STATE); |
| 162 | + TLV::TLVReader reader; |
108 | 163 |
|
109 | | - return app::DataModel::Decode(attributeData[0].dataReader, out); |
110 | | - } |
| 164 | + VerifyOrReturnValue(request.Encode(writer, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); |
| 165 | + VerifyOrReturnValue(writer.Finalize() == CHIP_NO_ERROR, result); |
111 | 166 |
|
112 | | - // Write attribute from `value` parameter. |
113 | | - // The `value` parameter must be of the correct type for the attribute being written. |
114 | | - // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::Type` for the `value` parameter to be spec |
115 | | - // compliant (see the comment of the class for usage example). |
116 | | - // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster. |
117 | | - // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
118 | | - template <typename T> |
119 | | - app::DataModel::ActionReturnStatus WriteAttribute(AttributeId attr_id, const T & value) |
120 | | - { |
121 | | - VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE); |
122 | | - auto path = mCluster.GetPaths()[0]; |
| 167 | + reader.Init(mTlvBuffer, writer.GetLengthWritten()); |
| 168 | + VerifyOrReturnValue(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); |
123 | 169 |
|
124 | | - app::Testing::WriteOperation writeOperation(path.mEndpointId, path.mClusterId, attr_id); |
| 170 | + result.status = mCluster.InvokeCommand(invokeRequest, reader, &mHandler); |
125 | 171 |
|
126 | | - app::AttributeValueDecoder decoder = writeOperation.DecoderFor(value); |
127 | | - return mCluster.WriteAttribute(writeOperation.GetRequest(), decoder); |
128 | | - } |
129 | | - |
130 | | - // Result structure for Invoke operations, containing both status and decoded response. |
131 | | - template <typename ResponseType> |
132 | | - struct InvokeResult { |
133 | | - std::optional<app::DataModel::ActionReturnStatus> status; |
134 | | - std::optional<ResponseType> response; |
135 | | - |
136 | | - // Returns true if the command was successful and response is available |
137 | | - bool IsSuccess() const { return status.has_value() && status->IsSuccess() && response.has_value(); } |
138 | | - }; |
139 | | - |
140 | | - // Invoke a command and return the decoded result. |
141 | | - // The `RequestType`, `ResponseType` type-parameters must be of the correct type for the command being invoked. |
142 | | - // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type` for the `RequestType` type-parameter to be spec compliant |
143 | | - // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type::ResponseType` for the `ResponseType` type-parameter to be |
144 | | - // spec compliant Will construct the command path using the first path returned by `GetPaths()` on the cluster. |
145 | | - // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path. |
146 | | - template <typename ResponseType, typename RequestType> |
147 | | - [[nodiscard]] InvokeResult<ResponseType> Invoke(chip::CommandId commandId, const RequestType & request) |
| 172 | + // If command was successful and there's a response, decode it (skip for NullObjectType) |
| 173 | + if constexpr (!std::is_same_v<ResponseType, app::DataModel::NullObjectType>) |
148 | 174 | { |
149 | | - InvokeResult<ResponseType> result; |
150 | | - |
151 | | - const auto & paths = mCluster.GetPaths(); |
152 | | - VerifyOrReturnValue(paths.size() == 1u, result); |
153 | | - |
154 | | - mHandler.ClearResponses(); |
155 | | - mHandler.ClearStatuses(); |
156 | | - |
157 | | - const app::DataModel::InvokeRequest invokeRequest = { .path = { paths[0].mEndpointId, paths[0].mClusterId, commandId } }; |
158 | | - |
159 | | - TLV::TLVWriter writer; |
160 | | - writer.Init(mTlvBuffer); |
161 | | - |
162 | | - TLV::TLVReader reader; |
163 | | - |
164 | | - VerifyOrReturnValue(request.Encode(writer, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); |
165 | | - VerifyOrReturnValue(writer.Finalize() == CHIP_NO_ERROR, result); |
166 | | - |
167 | | - reader.Init(mTlvBuffer, writer.GetLengthWritten()); |
168 | | - VerifyOrReturnValue(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()) == CHIP_NO_ERROR, result); |
169 | | - |
170 | | - result.status = mCluster.InvokeCommand(invokeRequest, reader, &mHandler); |
171 | | - |
172 | | - // If command was successful and there's a response, decode it (skip for NullObjectType) |
173 | | - if constexpr (!std::is_same_v<ResponseType, app::DataModel::NullObjectType>) { |
174 | | - if (result.status.has_value() && result.status->IsSuccess() && mHandler.HasResponse()) { |
175 | | - ResponseType decodedResponse; |
176 | | - CHIP_ERROR decodeError = mHandler.DecodeResponse(decodedResponse); |
177 | | - if (decodeError == CHIP_NO_ERROR) { |
178 | | - result.response = std::move(decodedResponse); |
179 | | - } else { |
180 | | - // Decode failed; reflect error in status and log |
181 | | - result.status = app::DataModel::ActionReturnStatus(decodeError); |
182 | | - ChipLogError(Test, "DecodeResponse failed: %s", decodeError.AsString()); |
183 | | - } |
| 175 | + if (result.status.has_value() && result.status->IsSuccess() && mHandler.HasResponse()) |
| 176 | + { |
| 177 | + ResponseType decodedResponse; |
| 178 | + CHIP_ERROR decodeError = mHandler.DecodeResponse(decodedResponse); |
| 179 | + if (decodeError == CHIP_NO_ERROR) |
| 180 | + { |
| 181 | + result.response = std::move(decodedResponse); |
| 182 | + } |
| 183 | + else |
| 184 | + { |
| 185 | + // Decode failed; reflect error in status and log |
| 186 | + result.status = app::DataModel::ActionReturnStatus(decodeError); |
| 187 | + ChipLogError(Test, "DecodeResponse failed: %s", decodeError.AsString()); |
184 | 188 | } |
185 | 189 | } |
186 | | - |
187 | | - return result; |
188 | 190 | } |
189 | 191 |
|
190 | | - private: |
191 | | - bool VerifyClusterPathsValid() |
| 192 | + return result; |
| 193 | + } |
| 194 | + |
| 195 | +private: |
| 196 | + bool VerifyClusterPathsValid() |
| 197 | + { |
| 198 | + auto paths = mCluster.GetPaths(); |
| 199 | + if (paths.size() != 1) |
192 | 200 | { |
193 | | - auto paths = mCluster.GetPaths(); |
194 | | - if (paths.size() != 1) { |
195 | | - ChipLogError(Test, "cluster.GetPaths() did not return exactly one path"); |
196 | | - return false; |
197 | | - } |
198 | | - return true; |
| 201 | + ChipLogError(Test, "cluster.GetPaths() did not return exactly one path"); |
| 202 | + return false; |
199 | 203 | } |
200 | | - |
201 | | - TestServerClusterContext mTestServerClusterContext {}; |
202 | | - app::ServerClusterInterface & mCluster; |
203 | | - |
204 | | - // Buffer size for TLV encoding/decoding of command payloads. |
205 | | - // 256 bytes was chosen as a conservative upper bound for typical command payloads in tests. |
206 | | - // All command payloads used in tests must fit within this buffer; tests with larger payloads will fail. |
207 | | - // If protocol or test requirements change, this value may need to be increased. |
208 | | - static constexpr size_t kTlvBufferSize = 256; |
209 | | - |
210 | | - app::Testing::MockCommandHandler mHandler; |
211 | | - uint8_t mTlvBuffer[kTlvBufferSize]; |
212 | | - std::vector<std::unique_ptr<app::Testing::ReadOperation>> mReadOperations; |
213 | | - }; |
| 204 | + return true; |
| 205 | + } |
| 206 | + |
| 207 | + TestServerClusterContext mTestServerClusterContext{}; |
| 208 | + app::ServerClusterInterface & mCluster; |
| 209 | + |
| 210 | + // Buffer size for TLV encoding/decoding of command payloads. |
| 211 | + // 256 bytes was chosen as a conservative upper bound for typical command payloads in tests. |
| 212 | + // All command payloads used in tests must fit within this buffer; tests with larger payloads will fail. |
| 213 | + // If protocol or test requirements change, this value may need to be increased. |
| 214 | + static constexpr size_t kTlvBufferSize = 256; |
| 215 | + |
| 216 | + app::Testing::MockCommandHandler mHandler; |
| 217 | + uint8_t mTlvBuffer[kTlvBufferSize]; |
| 218 | + std::vector<std::unique_ptr<app::Testing::ReadOperation>> mReadOperations; |
| 219 | +}; |
214 | 220 |
|
215 | 221 | } // namespace Test |
216 | 222 | } // namespace chip |
0 commit comments