diff --git a/matter/src/interaction_model/core.rs b/matter/src/interaction_model/core.rs index bc22385..2540c02 100644 --- a/matter/src/interaction_model/core.rs +++ b/matter/src/interaction_model/core.rs @@ -44,7 +44,7 @@ use super::{messages::msg::TimedReq, InteractionConsumer}; /* Interaction Model ID as per the Matter Spec */ const PROTO_ID_INTERACTION_MODEL: usize = 0x01; -#[derive(FromPrimitive, Debug, Copy, Clone)] +#[derive(FromPrimitive, Debug, Copy, Clone, PartialEq)] pub enum OpCode { Reserved = 0, StatusResponse = 1, diff --git a/matter/src/interaction_model/messages.rs b/matter/src/interaction_model/messages.rs index e54bad0..c5c1ca0 100644 --- a/matter/src/interaction_model/messages.rs +++ b/matter/src/interaction_model/messages.rs @@ -71,19 +71,25 @@ pub mod msg { tlv::{FromTLV, TLVArray, TLVElement, TLVWriter, TagType, ToTLV}, }; - use super::ib::{AttrData, AttrPath, AttrResp, CmdData, DataVersionFilter}; + use super::ib::{self, AttrData, AttrPath, AttrResp, AttrStatus, CmdData, DataVersionFilter}; - #[derive(FromTLV)] + #[derive(FromTLV, ToTLV)] pub struct TimedReq { pub timeout: u16, } - #[derive(ToTLV)] + #[derive(FromTLV, ToTLV)] pub struct StatusResp { pub status: IMStatusCode, } - #[derive(FromTLV)] + pub enum InvReqTag { + SupressResponse = 0, + TimedReq = 1, + InvokeRequests = 2, + } + + #[derive(FromTLV, ToTLV)] #[tlvargs(lifetime = "'a")] pub struct InvReq<'a> { pub suppress_response: Option, @@ -91,15 +97,18 @@ pub mod msg { pub inv_requests: Option>>, } + // This enum is helpful when we are constructing the response + // step by step in incremental manner pub enum InvRespTag { SupressResponse = 0, InvokeResponses = 1, } - pub enum InvReqTag { - SupressResponse = 0, - TimedReq = 1, - InvokeRequests = 2, + #[derive(FromTLV, ToTLV, Debug)] + #[tlvargs(lifetime = "'a")] + pub struct InvResp<'a> { + pub suppress_response: Option, + pub inv_responses: Option>>, } #[derive(Default, ToTLV, FromTLV)] @@ -171,6 +180,12 @@ pub mod msg { } // Write Response + #[derive(ToTLV, FromTLV)] + #[tlvargs(lifetime = "'a")] + pub struct WriteResp<'a> { + pub write_responses: TLVArray<'a, AttrStatus>, + } + pub enum WriteRespTag { WriteResponses = 0, } @@ -190,7 +205,7 @@ pub mod ib { use super::GenericPath; // Command Response - #[derive(Clone, Copy, FromTLV, ToTLV)] + #[derive(Clone, Copy, FromTLV, ToTLV, Debug)] #[tlvargs(lifetime = "'a")] pub enum InvResp<'a> { Cmd(CmdData<'a>), diff --git a/matter/src/tlv/traits.rs b/matter/src/tlv/traits.rs index a4d2ae3..ab03a63 100644 --- a/matter/src/tlv/traits.rs +++ b/matter/src/tlv/traits.rs @@ -19,6 +19,7 @@ use super::{ElementType, TLVContainerIterator, TLVElement, TLVWriter, TagType}; use crate::error::Error; use core::slice::Iter; use log::error; +use std::fmt::Debug; pub trait FromTLV<'a> { fn from_tlv(t: &TLVElement<'a>) -> Result @@ -282,6 +283,7 @@ impl TLVArrayOwned { } } +#[derive(Copy, Clone)] pub enum TLVArray<'a, T> { // This is used for the to-tlv path Slice(&'a [T]), @@ -342,6 +344,27 @@ impl<'a, T: FromTLV<'a> + Copy> Iterator for TLVArrayIter<'a, T> { } } +impl<'a, T> PartialEq<&[T]> for TLVArray<'a, T> +where + T: ToTLV + FromTLV<'a> + Copy + PartialEq, +{ + fn eq(&self, other: &&[T]) -> bool { + let mut iter1 = self.iter(); + let mut iter2 = other.into_iter(); + loop { + match (iter1.next(), iter2.next()) { + (None, None) => return true, + (Some(x), Some(y)) => { + if x != *y { + return false; + } + } + _ => return false, + } + } + } +} + impl<'a, T: ToTLV> ToTLV for TLVArray<'a, T> { fn to_tlv(&self, tw: &mut TLVWriter, tag_type: TagType) -> Result<(), Error> { match *self { @@ -364,6 +387,15 @@ impl<'a, T> FromTLV<'a> for TLVArray<'a, T> { } } +impl<'a, T: Debug + ToTLV + FromTLV<'a> + Copy> Debug for TLVArray<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for i in self.iter() { + writeln!(f, "{:?}", i)?; + } + writeln!(f, "") + } +} + #[cfg(test)] mod tests { use super::{FromTLV, OctetStr, TLVElement, TLVWriter, TagType, ToTLV}; diff --git a/matter/tests/common/attributes.rs b/matter/tests/common/attributes.rs index 42d4c83..1131c27 100644 --- a/matter/tests/common/attributes.rs +++ b/matter/tests/common/attributes.rs @@ -21,6 +21,7 @@ use matter::interaction_model::{messages::ib::AttrResp, messages::msg::ReportDat pub fn assert_attr_report(received: &ReportDataMsg, expected: &[AttrResp]) { let mut index = 0; + // We can't use assert_eq because it will also try to match data-version for inv_response in received.attr_reports.as_ref().unwrap().iter() { println!("Validating index {}", index); match expected[index] { @@ -34,14 +35,7 @@ pub fn assert_attr_report(received: &ReportDataMsg, expected: &[AttrResp]) { panic!("Invalid response, expected AttrRespIn::Data"); } }, - AttrResp::Status(e_s) => match inv_response { - AttrResp::Status(s) => { - assert_eq!(e_s, s); - } - _ => { - panic!("Invalid response, expected AttrRespIn::Status"); - } - }, + AttrResp::Status(s) => assert_eq!(AttrResp::Status(s), inv_response), } println!("Index {} success", index); index += 1; diff --git a/matter/tests/common/commands.rs b/matter/tests/common/commands.rs new file mode 100644 index 0000000..919565a --- /dev/null +++ b/matter/tests/common/commands.rs @@ -0,0 +1,98 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * 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. + */ + +use matter::{ + data_model::objects::EncodeValue, + interaction_model::{ + messages::ib::{CmdPath, CmdStatus, InvResp}, + messages::msg, + }, +}; + +pub enum ExpectedInvResp { + Cmd(CmdPath, u8), + Status(CmdStatus), +} + +pub fn assert_inv_response(resp: &msg::InvResp, expected: &[ExpectedInvResp]) { + let mut index = 0; + for inv_response in resp.inv_responses.unwrap().iter() { + println!("Validating index {}", index); + match expected[index] { + ExpectedInvResp::Cmd(e_c, e_d) => match inv_response { + InvResp::Cmd(c) => { + assert_eq!(e_c, c.path); + match c.data { + EncodeValue::Tlv(t) => { + assert_eq!(e_d, t.find_tag(0).unwrap().u8().unwrap()) + } + _ => panic!("Incorrect CmdDataType"), + } + } + _ => { + panic!("Invalid response, expected InvResponse::Cmd"); + } + }, + ExpectedInvResp::Status(e_status) => match inv_response { + InvResp::Status(status) => { + assert_eq!(e_status, status); + } + _ => { + panic!("Invalid response, expected InvResponse::Status"); + } + }, + } + println!("Index {} success", index); + index += 1; + } + assert_eq!(index, expected.len()); +} + +#[macro_export] +macro_rules! cmd_data { + ($path:ident, $data:literal) => { + CmdData::new($path, EncodeValue::Value(&($data as u32))) + }; +} + +#[macro_export] +macro_rules! echo_req { + ($endpoint:literal, $data:literal) => { + CmdData::new( + CmdPath::new( + Some($endpoint), + Some(echo_cluster::ID), + Some(echo_cluster::Commands::EchoReq as u16), + ), + EncodeValue::Value(&($data as u32)), + ) + }; +} + +#[macro_export] +macro_rules! echo_resp { + ($endpoint:literal, $data:literal) => { + ExpectedInvResp::Cmd( + CmdPath::new( + Some($endpoint), + Some(echo_cluster::ID), + Some(echo_cluster::Commands::EchoResp as u16), + ), + $data, + ) + }; +} diff --git a/matter/tests/common/im_engine.rs b/matter/tests/common/im_engine.rs index 1d1ac38..ddd8b30 100644 --- a/matter/tests/common/im_engine.rs +++ b/matter/tests/common/im_engine.rs @@ -28,7 +28,7 @@ use matter::{ }, error::Error, fabric::FabricMgr, - interaction_model::{core::OpCode, messages::ib::CmdPath, messages::msg, InteractionModel}, + interaction_model::{core::OpCode, InteractionModel}, tlv::{TLVWriter, TagType, ToTLV}, transport::packet::Packet, transport::proto_demux::HandleProto, @@ -58,20 +58,24 @@ pub struct ImEngine { pub dm: DataModel, pub acl_mgr: Arc, pub im: Box, + // By default, a new exchange is created for every run, if you wish to instead using a specific + // exchange, set this variable. This is helpful in situations where you have to run multiple + // actions in the same transaction (exchange) + pub exch: Option, } pub struct ImInput<'a> { action: OpCode, - data_in: &'a [u8], + data: &'a dyn ToTLV, peer_id: u64, } pub const IM_ENGINE_PEER_ID: u64 = 445566; impl<'a> ImInput<'a> { - pub fn new(action: OpCode, data_in: &'a [u8]) -> Self { + pub fn new(action: OpCode, data: &'a dyn ToTLV) -> Self { Self { action, - data_in, + data, peer_id: IM_ENGINE_PEER_ID, } } @@ -111,12 +115,19 @@ impl ImEngine { let im = Box::new(InteractionModel::new(Box::new(dm.clone()))); - Self { dm, acl_mgr, im } + Self { + dm, + acl_mgr, + im, + exch: None, + } } /// Run a transaction through the interaction model engine - pub fn process(&mut self, input: &ImInput, data_out: &mut [u8]) -> usize { - let mut exch = Exchange::new(1, 0, exchange::Role::Responder); + pub fn process<'a>(&mut self, input: &ImInput, data_out: &'a mut [u8]) -> (u8, &'a mut [u8]) { + let mut new_exch = Exchange::new(1, 0, exchange::Role::Responder); + // Choose whether to use a new exchange, or use the one from the ImEngine configuration + let mut exch = self.exch.as_mut().unwrap_or_else(|| &mut new_exch); let mut sess_mgr: SessionMgr = Default::default(); @@ -143,59 +154,39 @@ impl ImEngine { rx.set_proto_id(0x01); rx.set_proto_opcode(input.action as u8); rx.peer = Address::default(); - let in_data_len = input.data_in.len(); - let rx_buf = rx.as_borrow_slice(); - rx_buf[..in_data_len].copy_from_slice(input.data_in); - rx.get_parsebuf().unwrap().set_len(in_data_len); + + { + let mut buf = [0u8; 400]; + let buf_len = buf.len(); + let mut wb = WriteBuf::new(&mut buf, buf_len); + let mut tw = TLVWriter::new(&mut wb); + + input.data.to_tlv(&mut tw, TagType::Anonymous).unwrap(); + + let input_data = wb.as_borrow_slice(); + let in_data_len = input_data.len(); + let rx_buf = rx.as_borrow_slice(); + rx_buf[..in_data_len].copy_from_slice(input_data); + rx.get_parsebuf().unwrap().set_len(in_data_len); + } let mut ctx = ProtoCtx::new(exch_ctx, rx, tx); self.im.handle_proto_id(&mut ctx).unwrap(); let out_data_len = ctx.tx.as_borrow_slice().len(); data_out[..out_data_len].copy_from_slice(ctx.tx.as_borrow_slice()); - out_data_len + let response = ctx.tx.get_proto_opcode(); + (response, &mut data_out[..out_data_len]) } } // Create an Interaction Model, Data Model and run a rx/tx transaction through it -pub fn im_engine(action: OpCode, data_in: &[u8], data_out: &mut [u8]) -> (DataModel, usize) { +pub fn im_engine<'a>( + action: OpCode, + data: &dyn ToTLV, + data_out: &'a mut [u8], +) -> (DataModel, u8, &'a mut [u8]) { let mut engine = ImEngine::new(); - let input = ImInput::new(action, data_in); - let output_len = engine.process(&input, data_out); - (engine.dm, output_len) -} - -pub struct TestData<'a, 'b> { - tw: TLVWriter<'a, 'b>, -} - -impl<'a, 'b> TestData<'a, 'b> { - pub fn new(buf: &'b mut WriteBuf<'a>) -> Self { - Self { - tw: TLVWriter::new(buf), - } - } - - pub fn commands(&mut self, cmds: &[(CmdPath, Option)]) -> Result<(), Error> { - self.tw.start_struct(TagType::Anonymous)?; - self.tw.bool( - TagType::Context(msg::InvReqTag::SupressResponse as u8), - false, - )?; - self.tw - .bool(TagType::Context(msg::InvReqTag::TimedReq as u8), false)?; - self.tw - .start_array(TagType::Context(msg::InvReqTag::InvokeRequests as u8))?; - - for (cmd, data) in cmds { - self.tw.start_struct(TagType::Anonymous)?; - cmd.to_tlv(&mut self.tw, TagType::Context(0))?; - if let Some(d) = *data { - self.tw.u8(TagType::Context(1), d)?; - } - self.tw.end_container()?; - } - - self.tw.end_container()?; - self.tw.end_container() - } + let input = ImInput::new(action, data); + let (response, output) = engine.process(&input, data_out); + (engine.dm, response, output) } diff --git a/matter/tests/common/mod.rs b/matter/tests/common/mod.rs index 652a9d4..dea136a 100644 --- a/matter/tests/common/mod.rs +++ b/matter/tests/common/mod.rs @@ -16,5 +16,6 @@ */ pub mod attributes; +pub mod commands; pub mod echo_cluster; pub mod im_engine; diff --git a/matter/tests/data_model/acl_and_dataver.rs b/matter/tests/data_model/acl_and_dataver.rs index e44c536..11e3576 100644 --- a/matter/tests/data_model/acl_and_dataver.rs +++ b/matter/tests/data_model/acl_and_dataver.rs @@ -29,8 +29,7 @@ use matter::{ }, messages::{msg, GenericPath}, }, - tlv::{self, ElementType, FromTLV, TLVArray, TLVElement, TLVWriter, TagType, ToTLV}, - utils::writebuf::WriteBuf, + tlv::{self, ElementType, FromTLV, TLVArray, TLVElement, TLVWriter, TagType}, }; use crate::{ @@ -61,20 +60,13 @@ fn gen_read_reqs_output<'a>( dataver_filters: Option>, out_buf: &'a mut [u8], ) -> ReportDataMsg<'a> { - let mut buf = [0u8; 400]; - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut tw = TLVWriter::new(&mut wb); - let mut read_req = ReadReq::new(true).set_attr_requests(input); read_req.dataver_filters = dataver_filters; - read_req.to_tlv(&mut tw, TagType::Anonymous).unwrap(); - let mut input = ImInput::new(OpCode::ReadRequest, wb.as_borrow_slice()); + let mut input = ImInput::new(OpCode::ReadRequest, &read_req); input.set_peer_node_id(peer_node_id); - let out_buf_len = im.process(&input, out_buf); - let out_buf = &out_buf[..out_buf_len]; + let (_, out_buf) = im.process(&input, out_buf); tlv::print_tlv_list(out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); @@ -88,21 +80,13 @@ fn handle_write_reqs( input: &[AttrData], expected: &[AttrStatus], ) { - let mut buf = [0u8; 400]; let mut out_buf = [0u8; 400]; - - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut tw = TLVWriter::new(&mut wb); - let write_req = WriteReq::new(false, input); - write_req.to_tlv(&mut tw, TagType::Anonymous).unwrap(); - let mut input = ImInput::new(OpCode::WriteRequest, wb.as_borrow_slice()); + let mut input = ImInput::new(OpCode::WriteRequest, &write_req); input.set_peer_node_id(peer_node_id); - let out_buf_len = im.process(&input, &mut out_buf); + let (_, out_buf) = im.process(&input, &mut out_buf); - let out_buf = &out_buf[..out_buf_len]; tlv::print_tlv_list(out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); diff --git a/matter/tests/data_model/attribute_lists.rs b/matter/tests/data_model/attribute_lists.rs index 399d9a0..34b1353 100644 --- a/matter/tests/data_model/attribute_lists.rs +++ b/matter/tests/data_model/attribute_lists.rs @@ -19,14 +19,13 @@ use matter::{ data_model::{core::DataModel, objects::EncodeValue}, interaction_model::{ core::{IMStatusCode, OpCode}, + messages::GenericPath, messages::{ ib::{AttrData, AttrPath, AttrStatus}, - msg::WriteReq, + msg::{WriteReq, WriteResp}, }, - messages::{msg, GenericPath}, }, - tlv::{self, FromTLV, Nullable, TLVWriter, TagType, ToTLV}, - utils::writebuf::WriteBuf, + tlv::{self, FromTLV, Nullable}, }; use crate::common::{ @@ -36,37 +35,14 @@ use crate::common::{ // Helper for handling Write Attribute sequences fn handle_write_reqs(input: &[AttrData], expected: &[AttrStatus]) -> DataModel { - let mut buf = [0u8; 400]; let mut out_buf = [0u8; 400]; - - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut tw = TLVWriter::new(&mut wb); - let write_req = WriteReq::new(false, input); - write_req.to_tlv(&mut tw, TagType::Anonymous).unwrap(); - let (dm, out_buf_len) = im_engine(OpCode::WriteRequest, wb.as_borrow_slice(), &mut out_buf); - let out_buf = &out_buf[..out_buf_len]; + let (dm, _, out_buf) = im_engine(OpCode::WriteRequest, &write_req, &mut out_buf); tlv::print_tlv_list(out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); - - let mut index = 0; - let response_iter = root - .find_tag(msg::WriteRespTag::WriteResponses as u32) - .unwrap() - .confirm_array() - .unwrap() - .enter() - .unwrap(); - for response in response_iter { - println!("Validating index {}", index); - let status = AttrStatus::from_tlv(&response).unwrap(); - assert_eq!(expected[index], status); - println!("Index {} success", index); - index += 1; - } - assert_eq!(index, expected.len()); + let resp = WriteResp::from_tlv(&root).unwrap(); + assert_eq!(resp.write_responses, expected); dm } diff --git a/matter/tests/data_model/attributes.rs b/matter/tests/data_model/attributes.rs index aaa9a74..5e230c7 100644 --- a/matter/tests/data_model/attributes.rs +++ b/matter/tests/data_model/attributes.rs @@ -23,13 +23,13 @@ use matter::{ }, interaction_model::{ core::{IMStatusCode, OpCode}, + messages::GenericPath, messages::{ ib::{AttrData, AttrPath, AttrResp, AttrStatus}, - msg::{ReadReq, ReportDataMsg, WriteReq}, + msg::{ReadReq, ReportDataMsg, WriteReq, WriteResp}, }, - messages::{msg, GenericPath}, }, - tlv::{self, ElementType, FromTLV, TLVElement, TLVList, TLVWriter, TagType, ToTLV}, + tlv::{self, ElementType, FromTLV, TLVElement, TLVList, TLVWriter, TagType}, utils::writebuf::WriteBuf, }; @@ -46,16 +46,8 @@ fn handle_read_reqs(input: &[AttrPath], expected: &[AttrResp]) { // Helper for handling Read Req sequences fn gen_read_reqs_output<'a>(input: &[AttrPath], out_buf: &'a mut [u8]) -> ReportDataMsg<'a> { - let mut buf = [0u8; 400]; - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut tw = TLVWriter::new(&mut wb); - let read_req = ReadReq::new(true).set_attr_requests(input); - read_req.to_tlv(&mut tw, TagType::Anonymous).unwrap(); - - let (_, out_buf_len) = im_engine(OpCode::ReadRequest, wb.as_borrow_slice(), out_buf); - let out_buf = &out_buf[..out_buf_len]; + let (_, _, out_buf) = im_engine(OpCode::ReadRequest, &read_req, out_buf); tlv::print_tlv_list(out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); ReportDataMsg::from_tlv(&root).unwrap() @@ -63,37 +55,14 @@ fn gen_read_reqs_output<'a>(input: &[AttrPath], out_buf: &'a mut [u8]) -> Report // Helper for handling Write Attribute sequences fn handle_write_reqs(input: &[AttrData], expected: &[AttrStatus]) -> DataModel { - let mut buf = [0u8; 400]; let mut out_buf = [0u8; 400]; - - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut tw = TLVWriter::new(&mut wb); - let write_req = WriteReq::new(false, input); - write_req.to_tlv(&mut tw, TagType::Anonymous).unwrap(); - let (dm, out_buf_len) = im_engine(OpCode::WriteRequest, wb.as_borrow_slice(), &mut out_buf); - let out_buf = &out_buf[..out_buf_len]; - tlv::print_tlv_list(out_buf); + let (dm, _, out_buf) = im_engine(OpCode::WriteRequest, &write_req, &mut out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); + let response = WriteResp::from_tlv(&root).unwrap(); + assert_eq!(response.write_responses, expected); - let mut index = 0; - let response_iter = root - .find_tag(msg::WriteRespTag::WriteResponses as u32) - .unwrap() - .confirm_array() - .unwrap() - .enter() - .unwrap(); - for response in response_iter { - println!("Validating index {}", index); - let status = AttrStatus::from_tlv(&response).unwrap(); - assert_eq!(expected[index], status); - println!("Index {} success", index); - index += 1; - } - assert_eq!(index, expected.len()); dm } diff --git a/matter/tests/data_model/commands.rs b/matter/tests/data_model/commands.rs index 8fdc838..ef0b434 100644 --- a/matter/tests/data_model/commands.rs +++ b/matter/tests/data_model/commands.rs @@ -15,108 +15,39 @@ * limitations under the License. */ +use crate::{ + cmd_data, + common::{commands::*, echo_cluster, im_engine::im_engine}, + echo_req, echo_resp, +}; + use matter::{ data_model::{cluster_on_off, objects::EncodeValue}, interaction_model::{ core::{IMStatusCode, OpCode}, - messages::ib::{CmdPath, CmdStatus, InvResp}, - messages::msg, + messages::{ + ib::{CmdData, CmdPath, CmdStatus}, + msg, + msg::InvReq, + }, }, - tlv::{self, FromTLV}, - utils::writebuf::WriteBuf, + tlv::{self, FromTLV, TLVArray}, }; -use crate::common::{ - echo_cluster, - im_engine::{im_engine, TestData}, -}; - -enum ExpectedInvResp { - Cmd(CmdPath, u8), - Status(CmdStatus), -} - // Helper for handling Invoke Command sequences -fn handle_commands(input: &[(CmdPath, Option)], expected: &[ExpectedInvResp]) { - let mut buf = [0u8; 400]; +fn handle_commands(input: &[CmdData], expected: &[ExpectedInvResp]) { let mut out_buf = [0u8; 400]; + let req = InvReq { + suppress_response: Some(false), + timed_request: Some(false), + inv_requests: Some(TLVArray::Slice(input)), + }; - let buf_len = buf.len(); - let mut wb = WriteBuf::new(&mut buf, buf_len); - let mut td = TestData::new(&mut wb); - - td.commands(input).unwrap(); - - let (_, out_buf_len) = im_engine(OpCode::InvokeRequest, wb.as_borrow_slice(), &mut out_buf); - let out_buf = &out_buf[..out_buf_len]; + let (_, _, out_buf) = im_engine(OpCode::InvokeRequest, &req, &mut out_buf); tlv::print_tlv_list(out_buf); let root = tlv::get_root_node_struct(out_buf).unwrap(); - - let mut index = 0; - let cmd_list_iter = root - .find_tag(msg::InvRespTag::InvokeResponses as u32) - .unwrap() - .confirm_array() - .unwrap() - .enter() - .unwrap(); - for response in cmd_list_iter { - println!("Validating index {}", index); - let inv_response = InvResp::from_tlv(&response).unwrap(); - match expected[index] { - ExpectedInvResp::Cmd(e_c, e_d) => match inv_response { - InvResp::Cmd(c) => { - assert_eq!(e_c, c.path); - match c.data { - EncodeValue::Tlv(t) => { - assert_eq!(e_d, t.find_tag(0).unwrap().u8().unwrap()) - } - _ => panic!("Incorrect CmdDataType"), - } - } - _ => { - panic!("Invalid response, expected InvResponse::Cmd"); - } - }, - ExpectedInvResp::Status(e_status) => match inv_response { - InvResp::Status(status) => { - assert_eq!(e_status, status); - } - _ => { - panic!("Invalid response, expected InvResponse::Status"); - } - }, - } - println!("Index {} success", index); - index += 1; - } - assert_eq!(index, expected.len()); -} - -macro_rules! echo_req { - ($endpoint:literal, $data:literal) => { - ( - CmdPath::new( - Some($endpoint), - Some(echo_cluster::ID), - Some(echo_cluster::Commands::EchoReq as u16), - ), - Some($data), - ) - }; -} - -macro_rules! echo_resp { - ($endpoint:literal, $data:literal) => { - ExpectedInvResp::Cmd( - CmdPath::new( - Some($endpoint), - Some(echo_cluster::ID), - Some(echo_cluster::Commands::EchoResp as u16), - ), - $data, - ) - }; + let resp = msg::InvResp::from_tlv(&root).unwrap(); + assert_inv_response(&resp, expected) } #[test] @@ -159,11 +90,11 @@ fn test_invoke_cmds_unsupported_fields() { let invalid_command = CmdPath::new(Some(0), Some(echo_cluster::ID), Some(0x1234)); let invalid_command_wc_endpoint = CmdPath::new(None, Some(echo_cluster::ID), Some(0x1234)); let input = &[ - (invalid_endpoint, Some(5)), - (invalid_cluster, Some(5)), - (invalid_cluster_wc_endpoint, Some(5)), - (invalid_command, Some(5)), - (invalid_command_wc_endpoint, Some(5)), + cmd_data!(invalid_endpoint, 5), + cmd_data!(invalid_cluster, 5), + cmd_data!(invalid_cluster_wc_endpoint, 5), + cmd_data!(invalid_command, 5), + cmd_data!(invalid_command_wc_endpoint, 5), ]; let expected = &[ @@ -191,15 +122,12 @@ fn test_invoke_cmd_wc_endpoint_all_have_clusters() { // 1 echo Request with wildcard endpoint // should generate 2 responses from the echo clusters on both let _ = env_logger::try_init(); - - let input = &[( - CmdPath::new( - None, - Some(echo_cluster::ID), - Some(echo_cluster::Commands::EchoReq as u16), - ), - Some(5), - )]; + let path = CmdPath::new( + None, + Some(echo_cluster::ID), + Some(echo_cluster::Commands::EchoReq as u16), + ); + let input = &[cmd_data!(path, 5)]; let expected = &[echo_resp!(0, 10), echo_resp!(1, 15)]; handle_commands(input, expected); } @@ -220,7 +148,7 @@ fn test_invoke_cmd_wc_endpoint_only_1_has_cluster() { Some(cluster_on_off::ID), Some(cluster_on_off::Commands::On as u16), ); - let input = &[(target, Some(1))]; + let input = &[cmd_data!(target, 1)]; let expected = &[ExpectedInvResp::Status(CmdStatus::new( expected_path, IMStatusCode::Sucess, diff --git a/matter/tests/data_model/timed_requests.rs b/matter/tests/data_model/timed_requests.rs new file mode 100644 index 0000000..75af2e4 --- /dev/null +++ b/matter/tests/data_model/timed_requests.rs @@ -0,0 +1,284 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * 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. + */ + +use core::time; +use std::thread; + +use matter::{ + data_model::{ + core::DataModel, + objects::{AttrValue, EncodeValue}, + }, + interaction_model::{ + core::{IMStatusCode, OpCode}, + messages::{ib::CmdData, ib::CmdPath, msg::InvReq, GenericPath}, + messages::{ + ib::{AttrData, AttrPath, AttrStatus}, + msg::{self, StatusResp, TimedReq, WriteReq, WriteResp}, + }, + }, + tlv::{self, FromTLV, TLVArray, TLVWriter, ToTLV}, + transport::exchange::{self, Exchange}, +}; + +use crate::{ + common::{ + commands::*, + echo_cluster, + im_engine::{ImEngine, ImInput}, + }, + echo_req, echo_resp, +}; + +fn handle_timed_reqs<'a>( + opcode: OpCode, + request: &dyn ToTLV, + timeout: u16, + delay: u16, + output: &'a mut [u8], +) -> (u8, DataModel, &'a [u8]) { + let mut im_engine = ImEngine::new(); + // Use the same exchange for all parts of the transaction + im_engine.exch = Some(Exchange::new(1, 0, exchange::Role::Responder)); + + if timeout != 0 { + // Send Timed Req + let mut tmp_buf = [0u8; 400]; + let timed_req = TimedReq { timeout }; + let im_input = ImInput::new(OpCode::TimedRequest, &timed_req); + let (_, out_buf) = im_engine.process(&im_input, &mut tmp_buf); + tlv::print_tlv_list(out_buf); + } else { + println!("Skipping timed request"); + } + + // Process any delays + let delay = time::Duration::from_millis(delay.into()); + thread::sleep(delay); + + // Send Write Req + let input = ImInput::new(opcode, request); + let (resp_opcode, output) = im_engine.process(&input, output); + (resp_opcode, im_engine.dm, output) +} +enum WriteResponse<'a> { + TransactionError, + TransactionSuccess(&'a [AttrStatus]), +} + +// Helper for handling Write Attribute sequences +fn handle_timed_write_reqs( + input: &[AttrData], + expected: WriteResponse, + timeout: u16, + delay: u16, +) -> DataModel { + let mut out_buf = [0u8; 400]; + let write_req = WriteReq::new(false, input); + + let (resp_opcode, dm, out_buf) = handle_timed_reqs( + OpCode::WriteRequest, + &write_req, + timeout, + delay, + &mut out_buf, + ); + tlv::print_tlv_list(out_buf); + let root = tlv::get_root_node_struct(out_buf).unwrap(); + + match expected { + WriteResponse::TransactionSuccess(t) => { + assert_eq!( + num::FromPrimitive::from_u8(resp_opcode), + Some(OpCode::WriteResponse) + ); + let resp = WriteResp::from_tlv(&root).unwrap(); + assert_eq!(resp.write_responses, t); + } + WriteResponse::TransactionError => { + assert_eq!( + num::FromPrimitive::from_u8(resp_opcode), + Some(OpCode::StatusResponse) + ); + let status_resp = StatusResp::from_tlv(&root).unwrap(); + assert_eq!(status_resp.status, IMStatusCode::Timeout); + } + } + dm +} + +#[test] +fn test_timed_write_fail_and_success() { + // - 1 Timed Attr Write Transaction should fail due to timeout + // - 1 Timed Attr Write Transaction should succeed + let val0 = 10; + let _ = env_logger::try_init(); + let attr_data0 = |tag, t: &mut TLVWriter| { + let _ = t.u16(tag, val0); + }; + + let ep_att = GenericPath::new( + None, + Some(echo_cluster::ID), + Some(echo_cluster::Attributes::AttWrite as u32), + ); + let input = &[AttrData::new( + None, + AttrPath::new(&ep_att), + EncodeValue::Closure(&attr_data0), + )]; + + let ep0_att = GenericPath::new( + Some(0), + Some(echo_cluster::ID), + Some(echo_cluster::Attributes::AttWrite as u32), + ); + + let ep1_att = GenericPath::new( + Some(1), + Some(echo_cluster::ID), + Some(echo_cluster::Attributes::AttWrite as u32), + ); + let expected = &[ + AttrStatus::new(&ep0_att, IMStatusCode::Sucess, 0), + AttrStatus::new(&ep1_att, IMStatusCode::Sucess, 0), + ]; + + // Test with incorrect handling + handle_timed_write_reqs(input, WriteResponse::TransactionError, 400, 500); + + // Test with correct handling + let dm = handle_timed_write_reqs(input, WriteResponse::TransactionSuccess(expected), 400, 0); + assert_eq!( + AttrValue::Uint16(val0), + dm.read_attribute_raw( + 0, + echo_cluster::ID, + echo_cluster::Attributes::AttWrite as u16 + ) + .unwrap() + ); + assert_eq!( + AttrValue::Uint16(val0), + dm.read_attribute_raw( + 0, + echo_cluster::ID, + echo_cluster::Attributes::AttWrite as u16 + ) + .unwrap() + ); +} + +enum TimedInvResponse<'a> { + TransactionError(IMStatusCode), + TransactionSuccess(&'a [ExpectedInvResp]), +} +// Helper for handling Invoke Command sequences +fn handle_timed_commands( + input: &[CmdData], + expected: TimedInvResponse, + timeout: u16, + delay: u16, + set_timed_request: bool, +) -> DataModel { + let mut out_buf = [0u8; 400]; + let req = InvReq { + suppress_response: Some(false), + timed_request: Some(set_timed_request), + inv_requests: Some(TLVArray::Slice(input)), + }; + + let (resp_opcode, dm, out_buf) = + handle_timed_reqs(OpCode::InvokeRequest, &req, timeout, delay, &mut out_buf); + tlv::print_tlv_list(out_buf); + let root = tlv::get_root_node_struct(out_buf).unwrap(); + + match expected { + TimedInvResponse::TransactionSuccess(t) => { + assert_eq!( + num::FromPrimitive::from_u8(resp_opcode), + Some(OpCode::InvokeResponse) + ); + let resp = msg::InvResp::from_tlv(&root).unwrap(); + assert_inv_response(&resp, t) + } + TimedInvResponse::TransactionError(e) => { + assert_eq!( + num::FromPrimitive::from_u8(resp_opcode), + Some(OpCode::StatusResponse) + ); + let status_resp = StatusResp::from_tlv(&root).unwrap(); + assert_eq!(status_resp.status, e); + } + } + dm +} + +#[test] +fn test_timed_cmd_success() { + // A timed request that works + let _ = env_logger::try_init(); + + let input = &[echo_req!(0, 5), echo_req!(1, 10)]; + let expected = &[echo_resp!(0, 10), echo_resp!(1, 30)]; + handle_timed_commands( + input, + TimedInvResponse::TransactionSuccess(expected), + 400, + 0, + true, + ); +} + +#[test] +fn test_timed_cmd_timeout() { + // A timed request that is executed after t imeout + let _ = env_logger::try_init(); + + let input = &[echo_req!(0, 5), echo_req!(1, 10)]; + handle_timed_commands( + input, + TimedInvResponse::TransactionError(IMStatusCode::Timeout), + 400, + 500, + true, + ); +} + +#[test] +fn test_timed_cmd_timedout_mismatch() { + // A timed request with timeout mismatch + let _ = env_logger::try_init(); + + let input = &[echo_req!(0, 5), echo_req!(1, 10)]; + handle_timed_commands( + input, + TimedInvResponse::TransactionError(IMStatusCode::TimedRequestMisMatch), + 400, + 0, + false, + ); + + let input = &[echo_req!(0, 5), echo_req!(1, 10)]; + handle_timed_commands( + input, + TimedInvResponse::TransactionError(IMStatusCode::TimedRequestMisMatch), + 0, + 0, + true, + ); +} diff --git a/matter/tests/data_model_tests.rs b/matter/tests/data_model_tests.rs index 318906e..64a6f90 100644 --- a/matter/tests/data_model_tests.rs +++ b/matter/tests/data_model_tests.rs @@ -22,4 +22,5 @@ mod data_model { mod attribute_lists; mod attributes; mod commands; + mod timed_requests; }