use crate::error::{EtherNetIpError, Result};
use crate::udt::{UdtDefinition, UdtMember};
use crate::EipClient;
use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
use tracing;
#[derive(Debug, Clone, PartialEq)]
pub enum TagScope {
Controller,
Program(String),
Global,
Local,
}
#[derive(Debug, Clone)]
pub struct ArrayInfo {
pub dimensions: Vec<u32>,
pub element_count: u32,
}
#[derive(Debug, Clone)]
pub struct TagMetadata {
pub data_type: u16,
pub size: u32,
pub is_array: bool,
pub dimensions: Vec<u32>,
pub permissions: TagPermissions,
pub scope: TagScope,
pub last_access: Instant,
pub array_info: Option<ArrayInfo>,
pub last_updated: Instant,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TagPermissions {
pub readable: bool,
pub writable: bool,
}
impl TagMetadata {
pub fn is_structure(&self) -> bool {
(0x00A0..=0x00AF).contains(&self.data_type)
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct TagCache {
tags: HashMap<String, (TagMetadata, Instant)>,
expiration: Duration,
}
impl TagCache {
#[allow(dead_code)]
pub fn new(expiration: Duration) -> Self {
Self {
tags: HashMap::new(),
expiration,
}
}
#[allow(dead_code)]
pub fn update_tag(&mut self, name: String, metadata: TagMetadata) {
self.tags.insert(name, (metadata, Instant::now()));
}
#[allow(dead_code)]
pub fn get_tag(&self, name: &str) -> Option<&TagMetadata> {
if let Some((metadata, timestamp)) = self.tags.get(name) {
if timestamp.elapsed() < self.expiration {
return Some(metadata);
}
}
None
}
#[allow(dead_code)]
pub fn cleanup(&mut self) {
self.tags
.retain(|_, (_, timestamp)| timestamp.elapsed() < self.expiration);
}
}
#[derive(Debug)]
pub struct TagManager {
pub cache: RwLock<HashMap<String, TagMetadata>>,
cache_duration: Duration,
pub udt_definitions: RwLock<HashMap<String, UdtDefinition>>,
}
impl TagManager {
pub fn new() -> Self {
Self {
cache: RwLock::new(HashMap::new()),
cache_duration: Duration::from_secs(300), udt_definitions: RwLock::new(HashMap::new()),
}
}
pub async fn get_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
let cache = self.cache.read().unwrap();
cache.get(tag_name).and_then(|metadata| {
if metadata.last_updated.elapsed() < self.cache_duration {
Some(metadata.clone())
} else {
None
}
})
}
pub async fn update_metadata(&self, tag_name: String, metadata: TagMetadata) {
self.cache.write().unwrap().insert(tag_name, metadata);
}
pub async fn validate_tag(
&self,
tag_name: &str,
required_permissions: &TagPermissions,
) -> Result<()> {
if let Some(metadata) = self.get_metadata(tag_name).await {
if !metadata.permissions.readable && required_permissions.readable {
return Err(EtherNetIpError::Permission(format!(
"Tag '{tag_name}' is not readable"
)));
}
if !metadata.permissions.writable && required_permissions.writable {
return Err(EtherNetIpError::Permission(format!(
"Tag '{tag_name}' is not writable"
)));
}
Ok(())
} else {
Err(EtherNetIpError::Tag(format!("Tag '{tag_name}' not found")))
}
}
pub async fn clear_cache(&self) {
self.cache.write().unwrap().clear();
}
pub async fn remove_stale_entries(&self) {
self.cache
.write()
.unwrap()
.retain(|_, metadata| metadata.last_updated.elapsed() < self.cache_duration);
}
pub async fn discover_tags(&self, client: &mut EipClient) -> Result<()> {
let response = client
.send_cip_request(&client.build_list_tags_request())
.await?;
let tags = self.parse_tag_list(&response)?;
let mut all_tags = Vec::new();
for (name, metadata) in tags {
all_tags.push((name, metadata));
}
let hierarchical_tags = self.discover_hierarchical_tags(client, &all_tags).await?;
let mut cache = self.cache.write().unwrap();
for (name, metadata) in hierarchical_tags {
cache.insert(name, metadata);
}
Ok(())
}
async fn discover_hierarchical_tags(
&self,
client: &mut EipClient,
base_tags: &[(String, TagMetadata)],
) -> Result<Vec<(String, TagMetadata)>> {
let mut all_tags = Vec::new();
let mut tag_names = std::collections::HashSet::new();
for (name, metadata) in base_tags {
if self.validate_tag_name(name) {
all_tags.push((name.clone(), metadata.clone()));
tag_names.insert(name.clone());
}
}
for (name, metadata) in base_tags {
if metadata.is_structure() && !metadata.is_array {
if let Ok(members) = self.discover_udt_members(client, name).await {
for (member_name, member_metadata) in members {
let full_name = format!("{}.{}", name, member_name);
if self.validate_tag_name(&full_name) && !tag_names.contains(&full_name) {
all_tags.push((full_name.clone(), member_metadata.clone()));
tag_names.insert(full_name.clone());
if member_metadata.is_structure() && !member_metadata.is_array {
if let Ok(nested_members) =
self.discover_udt_members(client, &full_name).await
{
for (nested_name, nested_metadata) in nested_members {
let nested_full_name =
format!("{}.{}", full_name, nested_name);
if self.validate_tag_name(&nested_full_name)
&& !tag_names.contains(&nested_full_name)
{
all_tags
.push((nested_full_name.clone(), nested_metadata));
tag_names.insert(nested_full_name);
}
}
}
}
}
}
}
}
}
tracing::debug!(
"Discovered {} total tags (including hierarchical)",
all_tags.len()
);
Ok(all_tags)
}
pub async fn discover_udt_members(
&self,
client: &mut EipClient,
udt_name: &str,
) -> Result<Vec<(String, TagMetadata)>> {
tracing::debug!("Discovering UDT members for: {}", udt_name);
if let Ok(udt_definition) = self.get_udt_definition(client, udt_name).await {
let mut members = Vec::new();
for member in &udt_definition.members {
let member_name = member.name.clone();
let full_name = format!("{}.{}", udt_name, member_name);
let metadata = TagMetadata {
data_type: member.data_type,
scope: TagScope::Controller,
permissions: TagPermissions {
readable: true,
writable: true,
},
is_array: false, dimensions: Vec::new(),
last_access: Instant::now(),
size: member.size,
array_info: None,
last_updated: Instant::now(),
};
if self.validate_tag_name(&full_name) {
members.push((full_name.clone(), metadata));
tracing::trace!(
"Found UDT member: {} (Type: 0x{:04X})",
full_name,
member.data_type
);
}
}
Ok(members)
} else {
tracing::warn!("Could not get UDT definition for: {}", udt_name);
Ok(Vec::new())
}
}
async fn get_udt_definition(
&self,
client: &mut EipClient,
udt_name: &str,
) -> Result<UdtDefinition> {
{
let definitions = self.udt_definitions.read().unwrap();
if let Some(definition) = definitions.get(udt_name) {
tracing::debug!("Using cached UDT definition for: {}", udt_name);
return Ok(definition.clone());
}
}
let cip_request = self.build_udt_definition_request(udt_name)?;
let response = client.send_cip_request(&cip_request).await?;
let definition = self.parse_udt_definition_response(&response, udt_name)?;
{
let mut definitions = self.udt_definitions.write().unwrap();
definitions.insert(udt_name.to_string(), definition.clone());
}
Ok(definition)
}
pub fn build_udt_definition_request(&self, udt_name: &str) -> Result<Vec<u8>> {
let mut request = Vec::new();
request.push(0x4C);
let path_size = 2 + (udt_name.len() + 1) / 2; request.push(path_size as u8);
request.push(0x91); request.push(udt_name.len() as u8);
request.extend_from_slice(udt_name.as_bytes());
if udt_name.len() % 2 != 0 {
request.push(0x00);
}
Ok(request)
}
pub fn parse_udt_definition_response(
&self,
response: &[u8],
udt_name: &str,
) -> Result<UdtDefinition> {
tracing::trace!(
"Parsing UDT definition response for {} ({} bytes): {:02X?}",
udt_name,
response.len(),
response
);
let mut definition = UdtDefinition {
name: udt_name.to_string(),
members: Vec::new(),
};
if response.len() > 10 {
let mut offset = 0;
let mut member_offset = 0u32;
while offset < response.len().saturating_sub(4) {
if let Some((data_type, size)) =
self.extract_data_type_from_response(&response[offset..])
{
let member_name = format!("Member_{}", definition.members.len() + 1);
definition.members.push(UdtMember {
name: member_name,
data_type,
offset: member_offset,
size,
});
member_offset += size;
offset += 4; } else {
offset += 1;
}
if definition.members.len() > 50 {
break;
}
}
}
if definition.members.is_empty() {
definition.members.push(UdtMember {
name: "Value".to_string(),
data_type: 0x00C4, offset: 0,
size: 4,
});
}
tracing::debug!(
"Parsed UDT definition with {} members",
definition.members.len()
);
Ok(definition)
}
fn extract_data_type_from_response(&self, data: &[u8]) -> Option<(u16, u32)> {
if data.len() < 4 {
return None;
}
let data_type = u16::from_le_bytes([data[0], data[1]]);
match data_type {
0x00C1 => Some((0x00C1, 1)), 0x00C2 => Some((0x00C2, 1)), 0x00C3 => Some((0x00C3, 2)), 0x00C4 => Some((0x00C4, 4)), 0x00C5 => Some((0x00C5, 8)), 0x00C6 => Some((0x00C6, 1)), 0x00C7 => Some((0x00C7, 2)), 0x00C8 => Some((0x00C8, 4)), 0x00C9 => Some((0x00C9, 8)), 0x00CA => Some((0x00CA, 4)), 0x00CB => Some((0x00CB, 8)), 0x00CE => Some((0x00CE, 86)), _ => None,
}
}
fn validate_tag_name(&self, tag_name: &str) -> bool {
if tag_name.is_empty() || tag_name.trim().is_empty() {
return false;
}
let valid_tag_name_regex =
regex::Regex::new(r"^[a-zA-Z][a-zA-Z0-9]*(?:[._][a-zA-Z0-9]+)*$").unwrap();
if !valid_tag_name_regex.is_match(tag_name) {
return false;
}
if tag_name.starts_with(char::is_numeric) {
return false;
}
if tag_name.contains("__") || tag_name.contains("..") {
return false;
}
true
}
pub fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
let definitions = self.udt_definitions.read().unwrap();
definitions.get(udt_name).cloned()
}
pub fn list_udt_definitions(&self) -> Vec<String> {
let definitions = self.udt_definitions.read().unwrap();
definitions.keys().cloned().collect()
}
pub fn clear_udt_cache(&self) {
let mut definitions = self.udt_definitions.write().unwrap();
definitions.clear();
}
pub fn parse_tag_list(&self, response: &[u8]) -> Result<Vec<(String, TagMetadata)>> {
tracing::trace!(
"Raw tag list response ({} bytes): {:02X?}",
response.len(),
response
);
if response.len() >= 3 {
let service_reply = response[0];
let general_status = response[2];
if general_status != 0x00 {
let error_msg = match general_status {
0x01 => "Connection failure - Tag discovery may not be supported on this PLC",
0x04 => "Path segment error",
0x05 => "Path destination unknown",
0x16 => "Object does not exist",
_ => "Unknown CIP error",
};
return Err(crate::error::EtherNetIpError::Protocol(format!(
"CIP Error 0x{:02X} during tag discovery: {}. Some PLCs do not support tag discovery. Try reading tags directly by name.",
general_status, error_msg
)));
}
if service_reply != 0xD5 && service_reply != 0x55 {
if general_status == 0x00 {
tracing::warn!("Unexpected service reply 0x{:02X}, but status is 0x00, attempting to parse", service_reply);
}
}
}
let mut tags = Vec::new();
if response.len() < 8 {
return Err(crate::error::EtherNetIpError::Protocol(
"Response too short for tag list".to_string(),
));
}
let item_count = u32::from_le_bytes([response[4], response[5], response[6], response[7]]);
tracing::debug!("Detected item count: {}", item_count);
let mut offset = 8;
if response.len() > 4 {
let additional_status_size = response[3] as usize;
if additional_status_size > 0 {
offset += additional_status_size * 2; }
}
while offset < response.len() {
if offset + 4 > response.len() {
tracing::warn!("Not enough bytes for instance ID at offset {}", offset);
break;
}
let instance_id = u32::from_le_bytes([
response[offset],
response[offset + 1],
response[offset + 2],
response[offset + 3],
]);
offset += 4;
if offset + 2 > response.len() {
tracing::warn!("Not enough bytes for name length at offset {}", offset);
break;
}
let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
offset += 2;
if name_length > 1000 || name_length == 0 {
tracing::warn!(
"Invalid name length {} at offset {}, skipping entry",
name_length,
offset - 2
);
let mut found_next = false;
let search_start = offset;
for i in search_start..response.len().saturating_sub(4) {
if response[i] == 0x00
&& response[i + 1] == 0x00
&& response[i + 2] == 0x00
&& response[i + 3] == 0x00
{
offset = i;
found_next = true;
break;
}
}
if !found_next {
break;
}
continue;
}
if offset
.checked_add(name_length)
.map_or(true, |end| end > response.len())
{
tracing::warn!(
"Not enough bytes for tag name at offset {} (need {}, have {})",
offset,
name_length,
response.len() - offset
);
break;
}
let name = String::from_utf8_lossy(&response[offset..offset + name_length]).to_string();
offset += name_length;
if offset + 2 > response.len() {
tracing::warn!("Not enough bytes for tag type at offset {}", offset);
break;
}
let tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
offset += 2;
let (type_code, is_structure, array_dims, _reserved) = self.parse_tag_type(tag_type);
let is_array = array_dims > 0;
let dimensions = if is_array {
vec![0; array_dims as usize] } else {
Vec::new()
};
let array_info = if is_array && !dimensions.is_empty() {
Some(ArrayInfo {
element_count: dimensions.iter().product(),
dimensions: dimensions.clone(),
})
} else {
None
};
if !self.is_valid_tag_type(type_code) {
tracing::debug!(
"Skipping tag {} - unsupported type 0x{:04X}",
name,
type_code
);
continue;
}
let metadata = TagMetadata {
data_type: type_code,
scope: TagScope::Controller,
permissions: TagPermissions {
readable: true,
writable: true,
},
is_array,
dimensions,
last_access: Instant::now(),
size: 0,
array_info,
last_updated: Instant::now(),
};
tracing::trace!(
"Parsed tag: {} (ID: {}, Type: 0x{:04X}, Structure: {})",
name,
instance_id,
type_code,
is_structure
);
tags.push((name, metadata));
}
tracing::debug!("Parsed {} tags from response", tags.len());
Ok(tags)
}
fn parse_tag_type(&self, tag_type: u16) -> (u16, bool, u8, bool) {
let type_code = if (tag_type & 0x00ff) == 0xc1 {
0x00c1
} else {
tag_type & 0x0fff
};
let is_structure = (tag_type & 0x8000) != 0;
let array_dims = ((tag_type & 0x6000) >> 13) as u8;
let reserved = (tag_type & 0x1000) != 0;
(type_code, is_structure, array_dims, reserved)
}
fn is_valid_tag_type(&self, type_code: u16) -> bool {
match type_code {
0x00C1 => true, 0x00C2 => true, 0x00C3 => true, 0x00C4 => true, 0x00C5 => true, 0x00C6 => true, 0x00C7 => true, 0x00C8 => true, 0x00C9 => true, 0x00CA => true, 0x00CB => true, 0x00CE => true, _ => false, }
}
pub async fn drill_down_tags(
&self,
base_tags: &[(String, TagMetadata)],
) -> Result<Vec<(String, TagMetadata)>> {
let mut all_tags = Vec::new();
let mut tag_names = std::collections::HashSet::new();
for (tag_name, metadata) in base_tags {
self.drill_down_recursive(&mut all_tags, &mut tag_names, tag_name, metadata, "")?;
}
tracing::debug!(
"Drill down completed: {} total tags discovered",
all_tags.len()
);
Ok(all_tags)
}
fn drill_down_recursive(
&self,
all_tags: &mut Vec<(String, TagMetadata)>,
tag_names: &mut std::collections::HashSet<String>,
tag_name: &str,
metadata: &TagMetadata,
previous_name: &str,
) -> Result<()> {
if metadata.is_array {
return Ok(());
}
let new_name = if previous_name.is_empty() {
tag_name.to_string()
} else {
format!("{}.{}", previous_name, tag_name)
};
if metadata.is_structure() && !metadata.is_array {
if self.validate_tag_name(&new_name) && !tag_names.contains(&new_name) {
all_tags.push((new_name.clone(), metadata.clone()));
tag_names.insert(new_name);
}
} else {
if self.is_valid_tag_type(metadata.data_type)
&& self.validate_tag_name(&new_name)
&& !tag_names.contains(&new_name)
{
all_tags.push((new_name.clone(), metadata.clone()));
tag_names.insert(new_name);
}
}
Ok(())
}
}
impl Default for TagManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::udt::UdtMember;
#[test]
fn test_tag_cache_expiration() {
let mut cache = TagCache::new(Duration::from_secs(1));
let metadata = TagMetadata {
data_type: 0x00C1,
size: 1,
is_array: false,
dimensions: vec![],
permissions: TagPermissions {
readable: true,
writable: true,
},
scope: TagScope::Controller,
last_access: Instant::now(),
array_info: None,
last_updated: Instant::now(),
};
cache.update_tag("TestTag".to_string(), metadata);
assert!(cache.get_tag("TestTag").is_some());
std::thread::sleep(Duration::from_secs(2));
assert!(cache.get_tag("TestTag").is_none());
}
#[test]
fn test_tag_metadata_is_structure() {
let bool_metadata = TagMetadata {
data_type: 0x00C1,
size: 1,
is_array: false,
dimensions: vec![],
permissions: TagPermissions {
readable: true,
writable: true,
},
scope: TagScope::Controller,
last_access: Instant::now(),
array_info: None,
last_updated: Instant::now(),
};
assert!(!bool_metadata.is_structure());
let dint_metadata = TagMetadata {
data_type: 0x00C4,
size: 4,
is_array: false,
dimensions: vec![],
permissions: TagPermissions {
readable: true,
writable: true,
},
scope: TagScope::Controller,
last_access: Instant::now(),
array_info: None,
last_updated: Instant::now(),
};
assert!(!dint_metadata.is_structure());
let udt_metadata = TagMetadata {
data_type: 0x00A0,
size: 20,
is_array: false,
dimensions: vec![],
permissions: TagPermissions {
readable: true,
writable: true,
},
scope: TagScope::Controller,
last_access: Instant::now(),
array_info: None,
last_updated: Instant::now(),
};
assert!(udt_metadata.is_structure());
}
#[test]
fn test_validate_tag_name() {
let tag_manager = TagManager::new();
assert!(tag_manager.validate_tag_name("ValidTag"));
assert!(tag_manager.validate_tag_name("Valid_Tag"));
assert!(tag_manager.validate_tag_name("Valid.Tag"));
assert!(tag_manager.validate_tag_name("Valid123"));
assert!(tag_manager.validate_tag_name("Valid_Tag123"));
assert!(tag_manager.validate_tag_name("Valid.Tag123"));
assert!(!tag_manager.validate_tag_name("")); assert!(!tag_manager.validate_tag_name(" ")); assert!(!tag_manager.validate_tag_name("123Invalid")); assert!(!tag_manager.validate_tag_name("Invalid__Tag")); assert!(!tag_manager.validate_tag_name("Invalid..Tag")); assert!(!tag_manager.validate_tag_name("Invalid-Tag")); assert!(!tag_manager.validate_tag_name("Invalid Tag")); assert!(!tag_manager.validate_tag_name("Invalid@Tag")); }
#[test]
fn test_parse_tag_type() {
let tag_manager = TagManager::new();
let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C1);
assert_eq!(type_code, 0x00C1);
assert!(!is_structure);
assert_eq!(array_dims, 0);
assert!(!reserved);
let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x00C4);
assert_eq!(type_code, 0x00C4);
assert!(!is_structure);
assert_eq!(array_dims, 0);
assert!(!reserved);
let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x80A0);
assert_eq!(type_code, 0x00A0);
assert!(is_structure);
assert_eq!(array_dims, 0);
assert!(!reserved);
let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x20C4);
assert_eq!(type_code, 0x00C4);
assert!(!is_structure);
assert_eq!(array_dims, 1);
assert!(!reserved);
let (type_code, is_structure, array_dims, reserved) = tag_manager.parse_tag_type(0x40C4);
assert_eq!(type_code, 0x00C4);
assert!(!is_structure);
assert_eq!(array_dims, 2);
assert!(!reserved);
}
#[test]
fn test_extract_data_type_from_response() {
let tag_manager = TagManager::new();
let data = [0xC1, 0x00, 0x01, 0x00];
assert_eq!(
tag_manager.extract_data_type_from_response(&data),
Some((0x00C1, 1))
);
let data = [0xC4, 0x00, 0x04, 0x00];
assert_eq!(
tag_manager.extract_data_type_from_response(&data),
Some((0x00C4, 4))
);
let data = [0xCA, 0x00, 0x04, 0x00];
assert_eq!(
tag_manager.extract_data_type_from_response(&data),
Some((0x00CA, 4))
);
let data = [0xCE, 0x00, 0x56, 0x00];
assert_eq!(
tag_manager.extract_data_type_from_response(&data),
Some((0x00CE, 86))
);
let data = [0xFF, 0xFF, 0x00, 0x00];
assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
let data = [0xC1, 0x00];
assert_eq!(tag_manager.extract_data_type_from_response(&data), None);
}
#[test]
fn test_parse_udt_definition_response() {
let tag_manager = TagManager::new();
let empty_response = [];
let definition = tag_manager
.parse_udt_definition_response(&empty_response, "TestUDT")
.unwrap();
assert_eq!(definition.name, "TestUDT");
assert_eq!(definition.members.len(), 1);
assert_eq!(definition.members[0].name, "Value");
assert_eq!(definition.members[0].data_type, 0x00C4);
let response_data = [
0xC1, 0x00, 0x01, 0x00, 0xC4, 0x00, 0x04, 0x00, 0xCA, 0x00, 0x04, 0x00, ];
let definition = tag_manager
.parse_udt_definition_response(&response_data, "MotorData")
.unwrap();
assert_eq!(definition.name, "MotorData");
assert_eq!(definition.members.len(), 2); assert_eq!(definition.members[0].name, "Member_1");
assert_eq!(definition.members[0].data_type, 0x00C1);
assert_eq!(definition.members[1].name, "Member_2");
assert_eq!(definition.members[1].data_type, 0x00C4);
}
#[test]
fn test_build_udt_definition_request() {
let tag_manager = TagManager::new();
let request = tag_manager
.build_udt_definition_request("MotorData")
.unwrap();
assert_eq!(request[0], 0x4C); assert_eq!(request[1], 0x07); assert_eq!(request[2], 0x91); assert_eq!(request[3], 9); assert_eq!(&request[4..13], b"MotorData");
let request = tag_manager.build_udt_definition_request("Motor").unwrap();
assert_eq!(request[0], 0x4C); assert_eq!(request[1], 0x05); assert_eq!(request[2], 0x91); assert_eq!(request[3], 5); assert_eq!(&request[4..9], b"Motor");
assert_eq!(request[9], 0x00); }
#[test]
fn test_udt_definition_caching() {
let tag_manager = TagManager::new();
assert!(tag_manager.list_udt_definitions().is_empty());
let udt_def = UdtDefinition {
name: "TestUDT".to_string(),
members: vec![
UdtMember {
name: "Value1".to_string(),
data_type: 0x00C1,
offset: 0,
size: 1,
},
UdtMember {
name: "Value2".to_string(),
data_type: 0x00C4,
offset: 4,
size: 4,
},
],
};
{
let mut definitions = tag_manager.udt_definitions.write().unwrap();
definitions.insert("TestUDT".to_string(), udt_def);
}
let retrieved = tag_manager.get_udt_definition_cached("TestUDT");
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.name, "TestUDT");
assert_eq!(retrieved.members.len(), 2);
let udt_list = tag_manager.list_udt_definitions();
assert_eq!(udt_list.len(), 1);
assert_eq!(udt_list[0], "TestUDT");
tag_manager.clear_udt_cache();
assert!(tag_manager.list_udt_definitions().is_empty());
assert!(tag_manager.get_udt_definition_cached("TestUDT").is_none());
}
#[test]
fn test_parse_tag_list_with_invalid_data() {
let tag_manager = TagManager::new();
let invalid_response = [
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
let result = tag_manager.parse_tag_list(&invalid_response);
assert!(result.is_ok());
let tags = result.unwrap();
assert_eq!(tags.len(), 0); }
#[test]
fn test_parse_tag_list_with_valid_data() {
let tag_manager = TagManager::new();
let valid_response = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, b'M', b'o', b't', b'o', b'r', b'D', b'a', b't', 0xC4, 0x00, ];
let result = tag_manager.parse_tag_list(&valid_response);
assert!(result.is_ok());
let tags = result.unwrap();
assert!(!tags.is_empty() || tags.is_empty()); }
#[test]
fn test_tag_scope_enum() {
let controller_scope = TagScope::Controller;
assert_eq!(controller_scope, TagScope::Controller);
let program_scope = TagScope::Program("MainProgram".to_string());
match program_scope {
TagScope::Program(name) => assert_eq!(name, "MainProgram"),
_ => panic!("Expected Program scope"),
}
let global_scope = TagScope::Global;
assert_eq!(global_scope, TagScope::Global);
let local_scope = TagScope::Local;
assert_eq!(local_scope, TagScope::Local);
}
#[test]
fn test_array_info() {
let array_info = ArrayInfo {
dimensions: vec![10, 20],
element_count: 200,
};
assert_eq!(array_info.dimensions, vec![10, 20]);
assert_eq!(array_info.element_count, 200);
}
#[test]
fn test_tag_permissions() {
let permissions = TagPermissions {
readable: true,
writable: false,
};
assert!(permissions.readable);
assert!(!permissions.writable);
}
}