1use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
11
12use ciplink::cip::TagResult;
13use ciplink::{ErrorKind, TagError, Value};
14use indexmap::IndexMap;
15use regex::Regex;
16use serde_json::{Map, Value as Json, json};
17
18use crate::config::PlcConfig;
19use crate::plc::{Catalog, Factory, Kind, Node, Plc, TypeDef, atomic_size};
20
21const STRING_LEN: usize = 82;
22const STRING_SIZE: u64 = 88;
23
24#[derive(Debug)]
27pub struct MockState {
28 pub written: Mutex<HashMap<String, Value>>,
30 pub fail: AtomicBool,
32 pub clock: Mutex<Option<f64>>,
34 pub version: AtomicU64,
36}
37
38impl Default for MockState {
39 fn default() -> Self {
40 MockState {
41 written: Mutex::default(),
42 fail: AtomicBool::new(false),
43 clock: Mutex::new(None),
44 version: AtomicU64::new(1),
45 }
46 }
47}
48
49impl MockState {
50 pub fn set_clock(&self, now: Option<f64>) {
52 *lock(&self.clock) = now;
53 }
54
55 fn now(&self) -> f64 {
56 lock(&self.clock).unwrap_or_else(|| {
57 std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .map_or(0.0, |d| d.as_secs_f64())
60 })
61 }
62
63 fn get(&self, path: &str) -> Option<Value> {
64 lock(&self.written).get(&path.to_lowercase()).cloned()
65 }
66}
67
68fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
69 m.lock().unwrap_or_else(PoisonError::into_inner)
70}
71
72pub type MockStates = Arc<Mutex<HashMap<String, Arc<MockState>>>>;
74
75pub fn factory() -> (Factory, MockStates) {
77 let states = MockStates::default();
78 let shared = Arc::clone(&states);
79 let factory: Factory = Arc::new(move |cfg: &PlcConfig| {
80 let state = Arc::clone(lock(&shared).entry(cfg.name.clone()).or_default());
81 Ok(Box::new(MockPlc::new(cfg, state)?) as Box<dyn Plc>)
82 });
83 (factory, states)
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89enum Ty {
90 Atomic,
91 Struct,
92 Str,
93 Unknown,
95}
96
97#[derive(Clone, Debug)]
99struct Def {
100 type_name: String,
101 ty: Ty,
102 count: u64,
103}
104
105#[derive(Debug)]
106struct Udt {
107 members: IndexMap<String, Def>,
108 size: u64,
109}
110
111fn regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
112 cell.get_or_init(|| Regex::new(pattern).expect("valid regex"))
114}
115
116fn type_re() -> &'static Regex {
117 static RE: OnceLock<Regex> = OnceLock::new();
118 regex(
119 &RE,
120 r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]+)\s*\])?\s*$",
121 )
122}
123
124fn part_re() -> &'static Regex {
125 static RE: OnceLock<Regex> = OnceLock::new();
126 regex(
127 &RE,
128 r"^(?:(\.?)([A-Za-z_][A-Za-z0-9_]*)|\[([0-9]+(?:,[0-9]+)*)\]|\.([0-9]+)|\{([0-9]+)\})",
129 )
130}
131
132fn name_re() -> &'static Regex {
133 static RE: OnceLock<Regex> = OnceLock::new();
134 regex(&RE, r"[A-Za-z_][A-Za-z0-9_]*")
135}
136
137fn parse_type(text: &Json) -> Result<(String, u64), String> {
138 let bad = || format!("mock_tags: bad type {text}");
139 let caps = type_re()
140 .captures(text.as_str().ok_or_else(bad)?)
141 .ok_or_else(bad)?;
142 let count = match caps.get(2) {
143 Some(n) => n.as_str().parse().map_err(|_| bad())?,
144 None => 0,
145 };
146 Ok((caps[1].to_owned(), count))
147}
148
149fn key<'a, V>(map: &'a IndexMap<String, V>, name: &str) -> Option<&'a String> {
151 if let Some((k, _)) = map.get_key_value(name) {
152 return Some(k);
153 }
154 let lower = name.to_lowercase();
155 map.keys().find(|k| k.to_lowercase() == lower)
156}
157
158struct Loader<'a> {
160 file_types: &'a Map<String, Json>,
161 udts: IndexMap<String, Udt>,
162}
163
164impl Loader<'_> {
165 fn def(&mut self, text: &Json, stack: &mut Vec<String>) -> Result<Def, String> {
166 let (name, count) = parse_type(text)?;
167 let upper = name.to_uppercase();
168 let (type_name, ty) = if atomic_size(&upper).is_some() {
169 (upper, Ty::Atomic)
170 } else if upper == "STRING" {
171 (upper, Ty::Str)
172 } else if let Some(k) = self
173 .file_types
174 .keys()
175 .find(|k| k.to_lowercase() == name.to_lowercase())
176 {
177 let k = k.clone();
178 self.udt(&k, stack)?;
179 (k, Ty::Struct)
180 } else {
181 (name, Ty::Unknown)
182 };
183 Ok(Def {
184 type_name,
185 ty,
186 count,
187 })
188 }
189
190 fn udt(&mut self, name: &str, stack: &mut Vec<String>) -> Result<(), String> {
191 if self.udts.contains_key(name) {
192 return Ok(());
193 }
194 if stack.iter().any(|s| s == name) {
195 return Err(format!("mock_tags: {name} contains itself"));
196 }
197 let members = self.file_types[name]
198 .as_object()
199 .ok_or_else(|| format!("mock_tags: type {name} is not an object"))?;
200 stack.push(name.to_owned());
201 let mut defs = IndexMap::new();
202 let mut offset = 0;
203 for (member, text) in members {
204 let def = self.def(text, stack)?;
205 offset += self.size(&def) * def.count.max(1);
206 defs.insert(member.clone(), def);
207 }
208 stack.pop();
209 self.udts.insert(
210 name.to_owned(),
211 Udt {
212 members: defs,
213 size: (offset.div_ceil(4) * 4).max(4),
214 },
215 );
216 Ok(())
217 }
218
219 fn size(&self, def: &Def) -> u64 {
220 match def.ty {
221 Ty::Atomic => atomic_size(&def.type_name).unwrap_or(4),
222 Ty::Struct => self.udts.get(&def.type_name).map_or(4, |u| u.size),
223 Ty::Str => STRING_SIZE,
224 Ty::Unknown => 4,
225 }
226 }
227}
228
229fn int_range(type_name: &str) -> Option<(i128, i128)> {
232 Some(match type_name {
233 "SINT" => (i8::MIN.into(), i8::MAX.into()),
234 "INT" => (i16::MIN.into(), i16::MAX.into()),
235 "DINT" => (i32::MIN.into(), i32::MAX.into()),
236 "LINT" => (i64::MIN.into(), i64::MAX.into()),
237 "USINT" => (0, u8::MAX.into()),
238 "UINT" => (0, u16::MAX.into()),
239 "UDINT" => (0, u32::MAX.into()),
240 "ULINT" => (0, u64::MAX.into()),
241 _ => return None,
242 })
243}
244
245fn bit_count(type_name: &str) -> Option<u32> {
247 Some(match type_name {
248 "BYTE" => 8,
249 "WORD" => 16,
250 "DWORD" => 32,
251 "LWORD" => 64,
252 _ => return None,
253 })
254}
255
256fn is_float(type_name: &str) -> bool {
257 matches!(type_name, "REAL" | "LREAL")
258}
259
260fn wrap(type_name: &str, v: i128) -> Value {
262 if let Some(n) = bit_count(type_name) {
263 let word = v.rem_euclid(1i128 << n) as u64;
264 return Value::Array((0..n).map(|i| Value::Bool(word >> i & 1 == 1)).collect());
265 }
266 let Some((low, high)) = int_range(type_name) else {
267 return Value::LInt(v as i64);
268 };
269 let w = low + (v - low).rem_euclid(high - low + 1);
270 match type_name {
272 "SINT" => Value::SInt(w as i8),
273 "INT" => Value::Int(w as i16),
274 "DINT" => Value::DInt(w as i32),
275 "LINT" => Value::LInt(w as i64),
276 "USINT" => Value::USInt(w as u8),
277 "UINT" => Value::UInt(w as u16),
278 "UDINT" => Value::UDInt(w as u32),
279 _ => Value::ULInt(w as u64),
280 }
281}
282
283fn word_of(value: &Value) -> Option<u64> {
285 match value {
286 Value::Array(bits) => bits.iter().enumerate().try_fold(0u64, |word, (i, b)| {
287 Some(if b.as_bool()? { word | 1 << i } else { word })
288 }),
289 _ => value.as_i128().map(|v| v as u64),
290 }
291}
292
293fn truthy(value: &Value) -> bool {
294 match value {
295 Value::Bool(b) => *b,
296 _ => value.as_f64().is_some_and(|v| v != 0.0),
297 }
298}
299
300fn error(message: impl Into<String>, kind: ErrorKind) -> TagError {
301 TagError::new(message, kind)
302}
303
304fn invalid(type_name: &str, value: &Value) -> TagError {
305 error(
306 format!("Invalid value for {type_name} - {}", value.py_repr()),
307 ErrorKind::BadValue,
308 )
309}
310
311fn check(type_name: &str, value: &Value) -> Result<Value, TagError> {
313 let checked = if type_name == "BOOL" {
314 value.as_bool().map(Value::Bool)
315 } else if let Some((low, high)) = int_range(type_name) {
316 value
317 .as_i128()
318 .filter(|v| (low..=high).contains(v))
319 .map(|v| wrap(type_name, v))
320 } else if let Some(n) = bit_count(type_name) {
321 match value {
323 Value::Array(bits) if bits.len() != n as usize => {
324 return Err(error(
325 format!("{type_name} needs exactly {n} values, got {}", bits.len()),
326 ErrorKind::BadValue,
327 ));
328 }
329 Value::Array(_) => word_of(value).map(|w| wrap(type_name, w.into())),
330 _ => None,
331 }
332 } else if is_float(type_name) {
333 value
335 .as_f64()
336 .filter(|v| type_name == "LREAL" || !v.is_finite() || (*v as f32).is_finite())
337 .map(Value::LReal)
338 } else if type_name == "STRING" {
339 match value {
340 Value::String(s) if s.chars().count() <= STRING_LEN => Some(value.clone()),
341 _ => None,
342 }
343 } else {
344 None
345 };
346 checked.ok_or_else(|| invalid(type_name, value))
347}
348
349pub struct MockPlc {
353 tags: IndexMap<String, Def>,
354 udts: IndexMap<String, Udt>,
355 state: Arc<MockState>,
356}
357
358struct Resolved<'a> {
360 path: String,
362 def: &'a Def,
363 array: bool,
365 bit: Option<u64>,
366 count: Option<u64>,
367 run: (String, u64),
369}
370
371fn simulated(type_name: &str, value: f64) -> Value {
372 if is_float(type_name) {
373 Value::LReal((value * 1000.0).round() / 1000.0)
374 } else {
375 wrap(type_name, value.round_ties_even() as i128)
376 }
377}
378
379fn crc32(data: &[u8]) -> u32 {
380 let mut crc = !0u32;
381 for &byte in data {
382 crc ^= u32::from(byte);
383 for _ in 0..8 {
384 crc = if crc & 1 == 1 {
385 crc >> 1 ^ 0xEDB8_8320
386 } else {
387 crc >> 1
388 };
389 }
390 }
391 !crc
392}
393
394fn last_name(path: &str) -> &str {
395 name_re().find_iter(path).last().map_or("", |m| m.as_str())
396}
397
398impl MockPlc {
399 pub fn new(cfg: &PlcConfig, state: Arc<MockState>) -> Result<Self, String> {
401 let data = match &cfg.mock_tags {
402 Some(path) => {
403 let text = std::fs::read_to_string(path)
404 .map_err(|e| format!("mock_tags {}: {e}", path.display()))?;
405 serde_json::from_str(&text)
406 .map_err(|e| format!("mock_tags {}: {e}", path.display()))?
407 }
408 None => json!({}),
409 };
410 let Json::Object(data) = data else {
411 return Err("mock_tags: not a JSON object".into());
412 };
413 let empty = Map::new();
414 let (file_types, tags) = match data.get("tags") {
415 Some(Json::Object(tags)) => (
416 data.get("types")
417 .and_then(Json::as_object)
418 .unwrap_or(&empty),
419 tags,
420 ),
421 _ => (&empty, &data),
422 };
423 let mut loader = Loader {
424 file_types,
425 udts: IndexMap::new(),
426 };
427 let mut defs = IndexMap::new();
428 for (name, text) in tags {
429 let def = loader.def(text, &mut Vec::new())?;
430 defs.insert(name.clone(), def);
431 }
432 Ok(MockPlc {
433 tags: defs,
434 udts: loader.udts,
435 state,
436 })
437 }
438
439 pub fn state(&self) -> &Arc<MockState> {
441 &self.state
442 }
443
444 fn resolve(&self, name: &str) -> Result<Resolved<'_>, TagError> {
445 let missing = || error(format!("Tag doesn't exist - {name}"), ErrorKind::NotFound);
446 let bad_name = || error(format!("Invalid tag name - {name}"), ErrorKind::BadRequest);
447 if let Some(k) = key(&self.tags, name) {
448 let def = &self.tags[k];
449 return Ok(Resolved {
450 path: k.clone(),
451 def,
452 array: def.count > 0,
453 bit: None,
454 count: None,
455 run: (k.clone(), 0),
456 });
457 }
458 let (prefix, rest) = if name.len() >= 8
459 && name.is_char_boundary(8)
460 && name[..8].eq_ignore_ascii_case("program:")
461 {
462 let dot = name.find('.').ok_or_else(missing)?;
463 (&name[..=dot], &name[dot + 1..])
464 } else {
465 ("", name)
466 };
467 let mut parts = Vec::new();
468 let mut pos = 0;
469 while pos < rest.len() {
470 let caps = part_re().captures(&rest[pos..]).ok_or_else(bad_name)?;
471 if caps.get(2).is_some() && caps.get(1).is_some_and(|d| !d.is_empty()) != (pos > 0) {
472 return Err(bad_name());
473 }
474 pos += caps[0].len();
475 parts.push(caps);
476 }
477 let first = parts.first().and_then(|c| c.get(2)).ok_or_else(missing)?;
478 let k = key(&self.tags, &format!("{prefix}{}", first.as_str())).ok_or_else(missing)?;
479 let mut def = &self.tags[k];
480 let mut path = k.clone();
481 let (mut array, mut bit, mut count) = (def.count > 0, None, None);
482 let mut run = (path.clone(), 0);
483 let mut indexed = false;
484 let number = |s: &str| s.parse::<u64>().unwrap_or(u64::MAX);
485 for caps in &parts[1..] {
486 if bit.is_some() || count.is_some() {
487 return Err(bad_name());
488 }
489 if let Some(member) = caps.get(2) {
490 if array || def.ty != Ty::Struct {
491 return Err(missing());
492 }
493 let udt = self.udts.get(&def.type_name).ok_or_else(missing)?;
494 let m = key(&udt.members, member.as_str()).ok_or_else(missing)?;
495 path = format!("{path}.{m}");
496 def = &udt.members[m];
497 array = def.count > 0;
498 indexed = false;
499 } else if let Some(index) = caps.get(3) {
500 let index: Vec<u64> = index.as_str().split(',').map(number).collect();
501 if !array || index.len() != 1 || index[0] >= def.count {
502 return Err(missing());
503 }
504 run = (path.clone(), index[0]);
505 path = format!("{path}[{}]", index[0]);
506 array = false;
507 indexed = true;
508 } else if let Some(b) = caps.get(4) {
509 let size = int_range(&def.type_name)
510 .and(atomic_size(&def.type_name))
511 .or_else(|| bit_count(&def.type_name).map(|n| u64::from(n) / 8));
512 let Some(size) = size.filter(|_| !array && def.ty == Ty::Atomic) else {
513 return Err(error(
514 format!("No bits in {} - {name}", def.type_name),
515 ErrorKind::BadRequest,
516 ));
517 };
518 let b = number(b.as_str());
519 if b >= size * 8 {
520 return Err(missing());
521 }
522 bit = Some(b);
523 } else if let Some(c) = caps.get(5) {
524 let c = number(c.as_str());
525 if !(array || indexed) || c < 1 {
526 return Err(error(
527 format!("Invalid element count - {name}"),
528 ErrorKind::BadRequest,
529 ));
530 }
531 if array {
532 run = (path.clone(), 0);
533 }
534 if run.1.saturating_add(c) > def.count {
535 return Err(missing());
536 }
537 count = Some(c);
538 }
539 }
540 Ok(Resolved {
541 path,
542 def,
543 array,
544 bit,
545 count,
546 run,
547 })
548 }
549
550 fn read_one(&self, name: &str, now: f64) -> TagResult {
551 let result = (|| {
552 let r = self.resolve(name)?;
553 let type_name = r.def.type_name.as_str();
554 if let Some(count) = r.count {
555 let (array_path, start) = &r.run;
556 let values = (*start..start + count)
557 .map(|i| self.value(&format!("{array_path}[{i}]"), r.def, false, now))
558 .collect::<Result<_, _>>()?;
559 return Ok((Value::Array(values), format!("{type_name}[{count}]")));
560 }
561 if r.array {
562 return Ok((
564 self.value(&format!("{}[0]", r.path), r.def, false, now)?,
565 type_name.to_owned(),
566 ));
567 }
568 let value = self.value(&r.path, r.def, false, now)?;
569 if let Some(bit) = r.bit {
570 let word = word_of(&value).unwrap_or(0);
571 return Ok((Value::Bool(word >> bit & 1 == 1), "BOOL".to_owned()));
572 }
573 Ok((value, type_name.to_owned()))
574 })();
575 match result {
576 Ok((value, data_type)) => TagResult {
577 tag: name.to_owned(),
578 value: Some(value),
579 data_type: Some(data_type),
580 error: None,
581 },
582 Err(e) => TagResult {
583 tag: name.to_owned(),
584 value: None,
585 data_type: None,
586 error: Some(e),
587 },
588 }
589 }
590
591 fn value(&self, path: &str, def: &Def, array: bool, now: f64) -> Result<Value, TagError> {
593 if array {
594 return (0..def.count)
595 .map(|i| self.value(&format!("{path}[{i}]"), def, false, now))
596 .collect::<Result<_, _>>()
597 .map(Value::Array);
598 }
599 if def.ty == Ty::Struct {
600 if let Some(udt) = self.udts.get(&def.type_name) {
601 return udt
602 .members
603 .iter()
604 .map(|(m, sub)| {
605 Ok((
606 m.clone(),
607 self.value(&format!("{path}.{m}"), sub, sub.count > 0, now)?,
608 ))
609 })
610 .collect::<Result<_, _>>()
611 .map(Value::Struct);
612 }
613 }
614 self.leaf(path, def, now)
615 }
616
617 fn leaf(&self, path: &str, def: &Def, now: f64) -> Result<Value, TagError> {
619 if let Some(written) = self.state.get(path) {
620 return Ok(written);
621 }
622 let type_name = def.type_name.as_str();
623 if !matches!(def.ty, Ty::Atomic | Ty::Str) {
624 return Err(error(
625 format!("mock does not simulate {type_name}"),
626 ErrorKind::Other,
627 ));
628 }
629 let seed = crc32(path.to_lowercase().as_bytes());
630 let wave = (2.0 * std::f64::consts::PI * now / f64::from(10 + seed % 20)
631 + f64::from(seed % 628) / 100.0)
632 .sin();
633 let member = last_name(path);
634 let name = member.to_lowercase();
635 let parent = &path[..path.rfind('.').map_or(0, |i| i + 1)];
636 if def.ty == Ty::Str {
637 return Ok(Value::String(format!(
638 "{member} {}",
639 ((now / 5.0).floor() as i64).rem_euclid(100)
640 )));
641 }
642 if type_name == "BOOL" {
643 return Ok(Value::Bool(
644 !(name.ends_with("pb") || name == "running") && wave > 0.0,
645 ));
646 }
647 let value = if name.ends_with("sp") || name.contains("setpoint") {
648 f64::from(seed % 10 * 100)
649 } else if name.contains("temp") {
650 55.0 + 35.0 * wave
651 } else if name.contains("level") {
652 50.0 + 50.0 * wave
653 } else if name.contains("speed") || name.contains("rpm") {
654 self.follow(parent, member, now)?
655 .unwrap_or(50.0 + 50.0 * wave)
656 } else if name.contains("position") {
657 (now * 25.0 + f64::from(seed)).rem_euclid(1000.0)
658 } else if name.contains("count") {
659 ((now.trunc() as i64).rem_euclid(100_000) + i64::from(seed % 1000)) as f64
660 } else if (int_range(type_name).is_some() || bit_count(type_name).is_some())
661 && ["word", "status", "flags", "bits"]
662 .iter()
663 .any(|w| name.contains(w))
664 {
665 (0..4u32)
667 .map(|i| {
668 ((now / f64::from(2 + i)).trunc() as i64 + i64::from(seed >> i)).rem_euclid(2)
669 << i
670 })
671 .sum::<i64>() as f64
672 } else {
673 50.0 + 25.0 * wave
674 };
675 Ok(simulated(type_name, value))
676 }
677
678 fn follow(&self, parent: &str, member: &str, now: f64) -> Result<Option<f64>, TagError> {
681 let (Ok(sp), Ok(running)) = (
682 self.resolve(&format!("{parent}{member}SP")),
683 self.resolve(&format!("{parent}Running")),
684 ) else {
685 return Ok(None);
686 };
687 let bool_def = Def {
688 type_name: "BOOL".into(),
689 ty: Ty::Atomic,
690 count: 0,
691 };
692 if !truthy(&self.leaf(&running.path, &bool_def, now)?) {
693 return Ok(Some(0.0));
694 }
695 let target = self.leaf(&sp.path, sp.def, now)?.as_f64().unwrap_or(0.0);
696 Ok(Some(if target != 0.0 {
698 target + 0.2 * now.sin()
699 } else {
700 target
701 }))
702 }
703
704 fn writes(
708 &self,
709 path: &str,
710 def: &Def,
711 array: bool,
712 value: &Value,
713 member: bool,
714 ) -> Result<Vec<(String, Value)>, TagError> {
715 if array {
716 let length = def.count;
717 let one = std::slice::from_ref(value);
718 let values = match value {
719 Value::Array(items) if member => {
720 if items.len() as u64 != length {
721 return Err(error(
722 format!("Expected {length} values - {}", value.py_repr()),
723 ErrorKind::BadValue,
724 ));
725 }
726 items.as_slice()
727 }
728 _ if member => {
729 return Err(error(
730 format!("Expected {length} values - {}", value.py_repr()),
731 ErrorKind::BadValue,
732 ));
733 }
734 Value::Array(items) if bit_count(&def.type_name).is_none() => {
736 if items.len() != 1 {
737 return Err(error(
738 "Too many values for requested elements, expected 1",
739 ErrorKind::BadValue,
740 ));
741 }
742 items.as_slice()
743 }
744 _ => one,
745 };
746 let mut out = Vec::new();
747 for (i, v) in values.iter().enumerate() {
748 out.extend(self.writes(&format!("{path}[{i}]"), def, false, v, false)?);
749 }
750 return Ok(out);
751 }
752 if def.ty == Ty::Struct {
753 if let Some(udt) = self.udts.get(&def.type_name) {
754 let Value::Struct(members) = value else {
755 return Err(invalid(&def.type_name, value));
756 };
757 let mut out = Vec::new();
758 for (name, v) in members {
759 let Some(sub) = udt.members.get(name) else {
761 return Err(error(
762 format!("No member {name} in {}", def.type_name),
763 ErrorKind::BadValue,
764 ));
765 };
766 out.extend(self.writes(
767 &format!("{path}.{name}"),
768 sub,
769 sub.count > 0,
770 v,
771 true,
772 )?);
773 }
774 let missing: Vec<&str> = udt
775 .members
776 .keys()
777 .filter(|m| !members.contains_key(*m))
778 .map(String::as_str)
779 .collect();
780 if !missing.is_empty() {
781 return Err(error(
782 format!(
783 "Missing members {} of {}",
784 missing.join(", "),
785 def.type_name
786 ),
787 ErrorKind::BadValue,
788 ));
789 }
790 return Ok(out);
791 }
792 }
793 Ok(vec![(path.to_owned(), check(&def.type_name, value)?)])
794 }
795
796 fn write_one(
797 &self,
798 name: &str,
799 value: &Value,
800 now: f64,
801 ) -> Result<(Vec<(String, Value)>, String), TagError> {
802 let r = self.resolve(name)?;
803 if let Some(bit) = r.bit {
804 let current = self.value(&r.path, r.def, false, now)?;
805 let word = word_of(¤t).unwrap_or(0);
806 let word = match check("BOOL", value)? {
807 Value::Bool(true) => word | 1 << bit,
808 _ => word & !(1 << bit),
809 };
810 let word = if bit_count(&r.def.type_name).is_some() {
811 i128::from(word)
812 } else {
813 i128::from(word as i64) };
815 return Ok((vec![(r.path, wrap(&r.def.type_name, word))], "BOOL".into()));
816 }
817 let writes = if let Some(count) = r.count {
818 let items = match value {
819 Value::Array(items) if items.len() as u64 == count => items,
820 _ => {
821 return Err(error(
822 format!("Expected {count} values - {}", value.py_repr()),
823 ErrorKind::BadValue,
824 ));
825 }
826 };
827 let (array_path, start) = &r.run;
828 let mut out = Vec::new();
829 for (i, v) in (*start..).zip(items) {
830 out.extend(self.writes(&format!("{array_path}[{i}]"), r.def, false, v, false)?);
831 }
832 out
833 } else {
834 self.writes(&r.path, r.def, r.array, value, false)?
835 };
836 Ok((writes, r.def.type_name.clone()))
837 }
838}
839
840fn node(def: &Def, member: bool, instance_id: Option<u32>, udts: &IndexMap<String, Udt>) -> Node {
841 let (kind, structure_size) = match def.ty {
842 Ty::Struct => (Kind::Struct, udts.get(&def.type_name).map(|u| u.size)),
843 Ty::Str => (Kind::String, Some(STRING_SIZE)),
844 Ty::Atomic | Ty::Unknown => (Kind::Atomic, None),
845 };
846 Node {
847 type_name: def.type_name.clone(),
848 kind,
849 dims: if def.count > 0 {
850 vec![def.count]
851 } else {
852 Vec::new()
853 },
854 member,
855 instance_id,
856 structure_size,
857 }
858}
859
860impl Plc for MockPlc {
861 fn open(&mut self) -> Result<(), String> {
862 if self.state.fail.load(Ordering::SeqCst) {
863 return Err("mock: connection refused".into());
864 }
865 Ok(())
866 }
867
868 fn close(&mut self) {}
869
870 fn read(&mut self, tags: &[&str]) -> Result<Vec<TagResult>, String> {
871 if self.state.fail.load(Ordering::SeqCst) {
872 return Err("mock: connection lost".into());
873 }
874 let now = self.state.now();
875 Ok(tags.iter().map(|name| self.read_one(name, now)).collect())
876 }
877
878 fn write(&mut self, tag: &str, value: &Value) -> Result<TagResult, String> {
879 if self.state.fail.load(Ordering::SeqCst) {
880 return Err("mock: connection lost".into());
881 }
882 let now = self.state.now();
883 let (writes, data_type) = match self.write_one(tag, value, now) {
884 Ok(ok) => ok,
885 Err(e) => {
886 return Ok(TagResult {
887 tag: tag.to_owned(),
888 value: None,
889 data_type: None,
890 error: Some(e),
891 });
892 }
893 };
894 for (leaf, leaf_value) in writes {
895 let member = last_name(&leaf).to_lowercase();
896 let pushed = leaf_value == Value::Bool(true)
897 && (member.ends_with("startpb") || member.ends_with("stoppb"));
898 lock(&self.state.written).insert(leaf.to_lowercase(), leaf_value);
899 if pushed {
900 let sibling = format!("{}Running", &leaf[..leaf.rfind('.').map_or(0, |i| i + 1)]);
901 if let Ok(running) = self.resolve(&sibling) {
902 lock(&self.state.written).insert(
903 running.path.to_lowercase(),
904 Value::Bool(member.ends_with("startpb")),
905 );
906 }
907 }
908 }
909 Ok(TagResult {
910 tag: tag.to_owned(),
911 value: Some(value.clone()),
912 data_type: Some(data_type),
913 error: None,
914 })
915 }
916
917 fn catalog(&mut self) -> Catalog {
918 let tags = self
919 .tags
920 .iter()
921 .enumerate()
922 .map(|(i, (name, def))| {
923 let id = u32::try_from(i + 1).ok();
924 (name.clone(), node(def, false, id, &self.udts))
925 })
926 .collect();
927 let types = self
928 .udts
929 .iter()
930 .map(|(name, udt)| {
931 let members = udt
932 .members
933 .iter()
934 .map(|(m, def)| (m.clone(), node(def, true, None, &self.udts)))
935 .collect();
936 let t = TypeDef {
937 members,
938 public: udt.members.keys().cloned().collect(),
939 structure_size: udt.size,
940 aoi: None,
941 whole_readable: true,
942 };
943 (name.clone(), t)
944 })
945 .collect();
946 Catalog::new(self.version(), tags, types)
947 }
948
949 fn version(&mut self) -> Json {
950 json!(self.state.version.load(Ordering::SeqCst))
951 }
952
953 fn connection_size(&self) -> usize {
954 4000
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961 use crate::values::from_json;
962 use std::collections::HashSet;
963 use std::path::PathBuf;
964 use std::sync::atomic::AtomicUsize;
965
966 fn bench_json() -> Json {
967 json!({
968 "types": {"BenchData": {"Running": "BOOL", "StartPB": "BOOL", "StopPB": "BOOL", "SpeedSP": "REAL",
969 "Speed": "REAL", "Axis": "Axis", "StatusWord": "DINT", "Count": "DINT",
970 "Message": "STRING", "Recipe": "Recipe"},
971 "Axis": {"Position": "REAL", "Velocity": "REAL"},
972 "Recipe": {"Name": "STRING", "Setpoints": "REAL[4]"},
973 "Tank": {"Level": "REAL", "Temp": "REAL", "Valve": "BOOL"}},
974 "tags": {"Tank": "Tank[4]", "Trend": "REAL[16]", "Bench": "BenchData", "Program:Bench.Mode": "DINT",
975 "Small": "SINT", "Odd": "MyAOI"},
976 })
977 }
978
979 fn mock_file(data: &Json) -> PathBuf {
980 static N: AtomicUsize = AtomicUsize::new(0);
981 let path = std::env::temp_dir().join(format!(
982 "ciplink-mock-{}-{}.json",
983 std::process::id(),
984 N.fetch_add(1, Ordering::SeqCst)
985 ));
986 std::fs::write(&path, data.to_string()).expect("write mock file");
987 path
988 }
989
990 fn cfg(name: &str, path: Option<PathBuf>) -> PlcConfig {
991 PlcConfig {
992 name: name.into(),
993 mock_tags: path,
994 ..PlcConfig::default()
995 }
996 }
997
998 fn mock(data: &Json) -> MockPlc {
999 let plc =
1000 MockPlc::new(&cfg("PLC1", Some(mock_file(data))), Arc::default()).expect("mock driver");
1001 plc.state.set_clock(Some(1000.0));
1002 plc
1003 }
1004
1005 fn read(plc: &mut dyn Plc, name: &str) -> TagResult {
1006 plc.read(&[name]).expect("connected").remove(0)
1007 }
1008
1009 fn value(plc: &mut dyn Plc, name: &str) -> Value {
1010 let result = read(plc, name);
1011 assert!(result.error.is_none(), "{result:?}");
1012 result.value.expect("a value")
1013 }
1014
1015 fn kind(result: &TagResult) -> Option<ErrorKind> {
1016 result.error.as_ref().map(|e| e.kind)
1017 }
1018
1019 fn write(plc: &mut dyn Plc, name: &str, value: Json) -> TagResult {
1020 plc.write(name, &from_json(&value).expect("a value"))
1021 .expect("connected")
1022 }
1023
1024 fn num(v: &Value) -> f64 {
1025 v.as_f64().expect("a number")
1026 }
1027
1028 fn member<'a>(v: &'a Value, name: &str) -> &'a Value {
1029 match v {
1030 Value::Struct(m) => &m[name],
1031 _ => panic!("not a struct: {v:?}"),
1032 }
1033 }
1034
1035 fn items(v: &Value) -> &[Value] {
1036 match v {
1037 Value::Array(items) => items,
1038 _ => panic!("not an array: {v:?}"),
1039 }
1040 }
1041
1042 #[test]
1043 fn crc32_matches_zlib() {
1044 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
1045 assert_eq!(crc32(b""), 0);
1046 }
1047
1048 #[test]
1049 fn flat_file() {
1050 let mut plc = mock(
1051 &json!({"Speed": "REAL", "Run": "BOOL", "Count": "DINT", "Recipe": "UDT_R",
1052 "Line1.Level": "REAL"}),
1053 );
1054 let (mut speeds, mut runs) = (HashSet::new(), HashSet::new());
1055 for i in 0..40 {
1056 plc.state.set_clock(Some(f64::from(i) * 0.7));
1057 speeds.insert(num(&value(&mut plc, "Speed")).to_bits());
1058 runs.insert(value(&mut plc, "Run") == Value::Bool(true));
1059 assert!(matches!(value(&mut plc, "Count"), Value::DInt(_)));
1060 assert!((0.0..=100.0).contains(&num(&value(&mut plc, "Line1.Level"))));
1061 }
1062 assert!(speeds.len() > 10);
1063 assert_eq!(runs.len(), 2);
1064 let missing = read(&mut plc, "Nope");
1065 assert_eq!(kind(&missing), Some(ErrorKind::NotFound));
1066 assert!(
1067 missing
1068 .error
1069 .expect("error")
1070 .message
1071 .starts_with("Tag doesn't exist")
1072 );
1073 assert_eq!(kind(&read(&mut plc, "Recipe")), Some(ErrorKind::Other)); assert!(write(&mut plc, "Count", json!(7)).error.is_none());
1075 assert_eq!(value(&mut plc, "Count"), Value::DInt(7));
1076 let catalog = plc.catalog();
1077 assert_eq!(catalog.tags["Speed"].type_name, "REAL");
1078 assert!(catalog.types.is_empty());
1079 }
1080
1081 #[test]
1082 fn flat_dotted_names_follow_their_siblings() {
1083 let mut plc =
1084 mock(&json!({"Line1.Speed": "REAL", "Line1.SpeedSP": "REAL", "Line1.Running": "BOOL"}));
1085 assert_eq!(num(&value(&mut plc, "Line1.Speed")), 0.0);
1086 assert!(write(&mut plc, "line1.speedsp", json!(300)).error.is_none());
1087 assert!(
1088 write(&mut plc, "Line1.Running", json!(true))
1089 .error
1090 .is_none()
1091 );
1092 assert!((num(&value(&mut plc, "Line1.Speed")) - 300.0).abs() <= 0.2);
1093 let catalog = plc.catalog();
1094 assert_eq!(
1095 catalog.info("line1.speed").map(|n| n.type_name.as_str()),
1096 Some("REAL")
1097 );
1098 }
1099
1100 #[test]
1101 fn catalog_is_ciplink_shaped() {
1102 let mut plc = mock(&bench_json());
1103 let c = plc.catalog();
1104 let tank = &c.tags["Tank"];
1105 assert_eq!(
1106 (tank.kind, tank.dims.clone(), tank.type_name.as_str()),
1107 (Kind::Struct, vec![4], "Tank")
1108 );
1109 assert_eq!(
1110 (tank.structure_size, tank.instance_id, tank.member),
1111 (Some(12), Some(1), false)
1112 );
1113 assert_eq!(
1114 c.tags["Trend"],
1115 Node {
1116 type_name: "REAL".into(),
1117 kind: Kind::Atomic,
1118 dims: vec![16],
1119 member: false,
1120 instance_id: Some(2),
1121 structure_size: None,
1122 }
1123 );
1124 assert_eq!(c.types["Tank"].public, ["Level", "Temp", "Valve"]);
1125 assert_eq!(c.types["Tank"].structure_size, 12);
1126 assert_eq!(c.types["BenchData"].structure_size, 220);
1127 let setpoints = &c.types["Recipe"].members["Setpoints"];
1128 assert_eq!((setpoints.dims.clone(), setpoints.member), (vec![4], true));
1129 let message = &c.types["BenchData"].members["Message"];
1130 assert_eq!(
1131 (message.kind, message.structure_size),
1132 (Kind::String, Some(88))
1133 );
1134 assert!(!c.types.contains_key("STRING"));
1135 assert_eq!(c.tags["Program:Bench.Mode"].type_name, "DINT");
1136 assert_eq!(plc.connection_size(), 4000);
1137 assert_eq!(
1138 c.info("Tank[2].level").map(|n| n.type_name.as_str()),
1139 Some("REAL")
1140 );
1141 assert_eq!(
1142 c.info("bench.recipe.setpoints[1]").map(|n| n.dims.clone()),
1143 Some(vec![4])
1144 );
1145 assert_eq!(
1146 c.info("Program:Bench.Mode").map(|n| n.type_name.as_str()),
1147 Some("DINT")
1148 );
1149 for unknown in ["Nope", "Bench.Nope", "Bench.Count.3"] {
1150 assert!(c.info(unknown).is_none(), "{unknown}");
1151 }
1152 assert_eq!(c.version, json!(1));
1153 assert_eq!(plc.descriptions(&["Trend"]), Ok(vec![None]));
1154 }
1155
1156 #[test]
1157 fn reads() {
1158 let mut b = mock(&bench_json());
1159 let whole = value(&mut b, "Bench");
1160 let axis = member(&whole, "Axis");
1161 assert_eq!(
1162 member(axis, "Position"),
1163 &value(&mut b, "Bench.Axis.Position")
1164 );
1165 assert_eq!(
1166 member(axis, "Velocity"),
1167 &value(&mut b, "bench.axis.velocity")
1168 );
1169 let setpoints = member(member(&whole, "Recipe"), "Setpoints");
1170 assert_eq!(setpoints, &value(&mut b, "Bench.Recipe.Setpoints[0]{4}"));
1171 assert_eq!(
1172 &items(setpoints)[2],
1173 &value(&mut b, "Bench.Recipe.Setpoints[2]")
1174 );
1175 assert!(matches!(member(&whole, "Message"), Value::String(_)));
1176 assert_eq!(member(&whole, "Message"), &value(&mut b, "Bench.Message"));
1177 let trend: Vec<Value> = (0..16)
1178 .map(|i| value(&mut b, &format!("Trend[{i}]")))
1179 .collect();
1180 assert_eq!(
1181 value(&mut b, "Trend[3]{5}"),
1182 Value::Array(trend[3..8].to_vec())
1183 );
1184 assert_eq!(value(&mut b, "Trend{16}"), Value::Array(trend.clone()));
1185 assert_eq!(
1186 read(&mut b, "Trend[3]{5}").data_type.as_deref(),
1187 Some("REAL[5]")
1188 );
1189 assert_eq!(value(&mut b, "Trend"), trend[0]); let tanks = value(&mut b, "Tank{4}");
1191 for (i, t) in items(&tanks).iter().enumerate() {
1192 assert_eq!(
1193 member(t, "Level"),
1194 &value(&mut b, &format!("Tank[{i}].Level"))
1195 );
1196 assert!((0.0..=100.0).contains(&num(member(t, "Level"))));
1197 assert!((20.0..=90.0).contains(&num(member(t, "Temp"))));
1198 }
1199 let word = num(&value(&mut b, "Bench.StatusWord")) as i64;
1200 for i in 0..4 {
1201 assert_eq!(
1202 value(&mut b, &format!("Bench.StatusWord.{i}")),
1203 Value::Bool(word >> i & 1 == 1)
1204 );
1205 }
1206 assert_eq!(
1207 read(&mut b, "Bench.StatusWord.0").data_type.as_deref(),
1208 Some("BOOL")
1209 );
1210 assert!(matches!(
1211 value(&mut b, "Program:Bench.Mode"),
1212 Value::DInt(_)
1213 ));
1214 assert!(matches!(value(&mut b, "Small"), Value::SInt(_)));
1215 for (name, k) in [
1216 ("Tank[4].Level", ErrorKind::NotFound),
1217 ("Trend[14]{3}", ErrorKind::NotFound),
1218 ("Bench.Nope", ErrorKind::NotFound),
1219 ("Bench.Speed.1", ErrorKind::BadRequest),
1220 ("Bench.Count.32", ErrorKind::NotFound),
1221 ("Bench..X", ErrorKind::BadRequest),
1222 ("Bench.Axis{2}", ErrorKind::BadRequest),
1223 ("Odd", ErrorKind::Other),
1224 ] {
1225 assert_eq!(kind(&read(&mut b, name)), Some(k), "{name}");
1226 }
1227 let results = b.read(&["Trend[1]", "Nope"]).expect("connected");
1228 assert_eq!(
1229 results.iter().map(kind).collect::<Vec<_>>(),
1230 [None, Some(ErrorKind::NotFound)]
1231 );
1232 }
1233
1234 #[test]
1235 fn values_follow_the_names_and_the_clock() {
1236 let mut b = mock(&bench_json());
1237 assert_eq!(value(&mut b, "Bench.Running"), Value::Bool(false));
1238 assert_eq!(num(&value(&mut b, "Bench.Speed")), 0.0);
1239 let (count, position) = (
1240 num(&value(&mut b, "Bench.Count")),
1241 num(&value(&mut b, "Bench.Axis.Position")),
1242 );
1243 b.state.set_clock(Some(1002.0));
1244 assert_eq!(num(&value(&mut b, "Bench.Count")), count + 2.0);
1245 assert!((num(&value(&mut b, "Bench.Axis.Position")) - (position + 50.0)).abs() < 1e-6);
1246 assert!(
1247 write(&mut b, "Bench.SpeedSP", json!(1200.0))
1248 .error
1249 .is_none()
1250 );
1251 assert!(write(&mut b, "Bench.StartPB", json!(true)).error.is_none());
1252 assert_eq!(value(&mut b, "Bench.Running"), Value::Bool(true));
1253 assert!((num(&value(&mut b, "Bench.Speed")) - 1200.0).abs() <= 0.2);
1254 assert!(write(&mut b, "Bench.StartPB", json!(false)).error.is_none());
1255 assert_eq!(value(&mut b, "Bench.Running"), Value::Bool(true));
1256 write(&mut b, "Bench.StopPB", json!(1));
1257 assert_eq!(value(&mut b, "Bench.Running"), Value::Bool(false));
1258 assert_eq!(num(&value(&mut b, "Bench.Speed")), 0.0);
1259 let mut other =
1261 MockPlc::new(&cfg("PLC1", None), Arc::clone(&b.state)).expect("mock driver");
1262 assert_eq!(kind(&read(&mut other, "Nope")), Some(ErrorKind::NotFound));
1263 let temp = value(&mut b, "Tank[1].Temp");
1264 assert_eq!(value(&mut b, "tank[1].temp"), temp);
1265 assert!(matches!(temp, Value::LReal(v) if (v * 1000.0).round() / 1000.0 == v));
1266 }
1267
1268 #[test]
1269 fn writes_are_type_checked() {
1270 let mut b = mock(&bench_json());
1271 for (name, bad) in [
1272 ("Bench.Running", json!("x")),
1273 ("Bench.Running", json!(2)),
1274 ("Bench.Count", json!(1.5)),
1275 ("Bench.Count", json!(true)),
1276 ("Small", json!(300)),
1277 ("Bench.SpeedSP", json!("a")),
1278 ("Bench.SpeedSP", json!(true)),
1279 ("Bench.Message", json!("x".repeat(83))),
1280 ("Bench.Message", json!(5)),
1281 ("Trend[0]{2}", json!([1.0])),
1282 ("Bench.Recipe", json!({"Nope": 1})),
1283 (
1284 "Bench.Recipe",
1285 json!({"Name": "ok", "Setpoints": [1.0, 2.0, 3.0, "x"]}),
1286 ),
1287 ("Bench.Axis", json!(3)),
1288 ("Odd", json!(1)),
1289 ("Bench.Recipe", json!({"Name": "ok"})),
1290 (
1291 "Bench.Recipe",
1292 json!({"Name": "ok", "Setpoints": [1.0, 2.0]}),
1293 ),
1294 ("Trend", json!([1.0, 2.0])),
1295 (
1296 "Bench.Recipe",
1297 json!({"name": "ok", "setpoints": [1.0, 2.0, 3.0, 4.0]}),
1298 ),
1299 ("Trend[0]", json!(1e40)),
1300 ] {
1301 let result = write(&mut b, name, bad.clone());
1304 assert_eq!(kind(&result), Some(ErrorKind::BadValue), "{name} {bad}");
1305 }
1306 assert!(lock(&b.state.written).is_empty()); let result = write(&mut b, "Bench.Running", json!(2));
1308 assert_eq!(
1309 result.error.expect("error").message,
1310 "Invalid value for BOOL - 2"
1311 );
1312 assert_eq!(
1313 kind(&write(&mut b, "Nope", json!(1))),
1314 Some(ErrorKind::NotFound)
1315 );
1316 assert!(write(&mut b, "Bench.Running", json!(1)).error.is_none());
1317 assert_eq!(value(&mut b, "Bench.Running"), Value::Bool(true));
1318 assert!(
1319 write(&mut b, "Bench.Message", json!("x".repeat(82)))
1320 .error
1321 .is_none()
1322 );
1323 assert_eq!(
1324 value(&mut b, "Bench.Message"),
1325 Value::String("x".repeat(82))
1326 );
1327 assert!(write(&mut b, "Small", json!(-5)).error.is_none());
1328 assert_eq!(value(&mut b, "Small"), Value::SInt(-5));
1329 let recipe = json!({"Name": "Brass", "Setpoints": [1, 2.5, 3, 4]});
1330 assert!(write(&mut b, "Bench.Recipe", recipe).error.is_none());
1331 assert_eq!(
1332 member(&value(&mut b, "Bench.Recipe"), "Name"),
1333 &Value::String("Brass".into())
1334 );
1335 assert_eq!(
1336 value(&mut b, "Bench.Recipe.Setpoints[0]{2}"),
1337 Value::Array(vec![Value::LReal(1.0), Value::LReal(2.5)])
1338 );
1339 assert!(
1340 write(&mut b, "Trend[2]{3}", json!([7.0, 8.0, 9.0]))
1341 .error
1342 .is_none()
1343 );
1344 assert_eq!(
1345 items(&value(&mut b, "Trend[1]{4}"))[1..],
1346 [Value::LReal(7.0), Value::LReal(8.0), Value::LReal(9.0)]
1347 );
1348 let ok = write(&mut b, "Trend[2]", json!(1));
1349 assert_eq!(
1350 (ok.data_type.as_deref(), ok.value),
1351 (Some("REAL"), Some(Value::LInt(1)))
1352 );
1353 }
1354
1355 #[test]
1356 fn bit_writes_read_modify_write() {
1357 let mut b = mock(&bench_json());
1358 write(&mut b, "Bench.StatusWord", json!(0b1000));
1359 assert!(
1360 write(&mut b, "Bench.StatusWord.1", json!(true))
1361 .error
1362 .is_none()
1363 );
1364 assert!(
1365 write(&mut b, "Bench.StatusWord.31", json!(1))
1366 .error
1367 .is_none()
1368 );
1369 assert_eq!(
1370 value(&mut b, "Bench.StatusWord"),
1371 Value::DInt(0b1010 + i32::MIN)
1372 );
1373 assert!(
1374 write(&mut b, "Bench.StatusWord.3", json!(false))
1375 .error
1376 .is_none()
1377 );
1378 assert_eq!(
1379 value(&mut b, "Bench.StatusWord"),
1380 Value::DInt(0b10 + i32::MIN)
1381 );
1382 assert_eq!(
1383 kind(&write(&mut b, "Bench.StatusWord.2", json!(5))),
1384 Some(ErrorKind::BadValue)
1385 );
1386 }
1387
1388 fn bits(word: u64, n: u32) -> Json {
1389 json!((0..n).map(|i| word >> i & 1 == 1).collect::<Vec<_>>())
1390 }
1391
1392 #[test]
1393 fn bit_strings_are_lists_of_bools() {
1394 let mut plc = mock(
1395 &json!({"types": {"Drive": {"Flags": "DWORD", "Mask": "WORD"}},
1396 "tags": {"StatusBits": "DWORD", "B": "BYTE", "L": "LWORD", "D": "Drive"}}),
1397 );
1398 let c = plc.catalog();
1399 assert_eq!(c.types["Drive"].structure_size, 8);
1400 let status = read(&mut plc, "StatusBits");
1401 assert_eq!(status.data_type.as_deref(), Some("DWORD"));
1402 let status = status.value.expect("a value");
1403 assert_eq!(items(&status).len(), 32);
1404 assert!(items(&status)[4..].iter().all(|b| *b == Value::Bool(false))); for (i, bit) in items(&status)[..4].iter().enumerate() {
1406 assert_eq!(&value(&mut plc, &format!("StatusBits.{i}")), bit);
1407 }
1408 assert_eq!(items(&value(&mut plc, "B")).len(), 8);
1409 assert_eq!(items(&value(&mut plc, "L")).len(), 64);
1410 assert_eq!(items(&value(&mut plc, "D.Mask")).len(), 16);
1411
1412 assert!(write(&mut plc, "D.Flags", bits(0b101, 32)).error.is_none());
1413 assert_eq!(
1414 value(&mut plc, "D.Flags"),
1415 from_json(&bits(0b101, 32)).expect("bits")
1416 );
1417 assert!(write(&mut plc, "D.Flags.31", json!(true)).error.is_none());
1418 assert!(write(&mut plc, "D.Flags.0", json!(0)).error.is_none());
1419 assert_eq!(
1420 value(&mut plc, "D.Flags"),
1421 from_json(&bits(0b100 | 1 << 31, 32)).expect("bits")
1422 );
1423 assert_eq!(value(&mut plc, "D.Flags.2"), Value::Bool(true));
1424 assert!(
1425 write(
1426 &mut plc,
1427 "D",
1428 json!({"Flags": bits(1, 32), "Mask": bits(2, 16)})
1429 )
1430 .error
1431 .is_none()
1432 );
1433 assert!(write(&mut plc, "L.63", json!(true)).error.is_none());
1434 assert_eq!(items(&value(&mut plc, "L"))[63], Value::Bool(true));
1435
1436 let short = write(&mut plc, "B", bits(1, 7));
1437 assert_eq!(kind(&short), Some(ErrorKind::BadValue));
1438 assert_eq!(
1439 short.error.expect("error").message,
1440 "BYTE needs exactly 8 values, got 7"
1441 );
1442 assert_eq!(
1443 kind(&write(&mut plc, "B", json!(3))),
1444 Some(ErrorKind::BadValue)
1445 ); let mut odd = vec![json!(true); 8];
1447 odd[3] = json!(2);
1448 assert_eq!(
1449 kind(&write(&mut plc, "B", json!(odd))),
1450 Some(ErrorKind::BadValue)
1451 );
1452 assert_eq!(
1453 kind(&read(&mut plc, "D.Mask.16")),
1454 Some(ErrorKind::NotFound)
1455 );
1456 assert_eq!(kind(&read(&mut plc, "B.8")), Some(ErrorKind::NotFound));
1457 }
1458
1459 #[test]
1460 fn factory_shares_state_and_simulates_disconnects() {
1461 let path = mock_file(&bench_json());
1462 let (factory, states) = factory();
1463 let mut one = factory(&cfg("PLC1", Some(path.clone()))).expect("driver");
1464 let mut two = factory(&cfg("PLC1", Some(path.clone()))).expect("driver");
1465 let mut other = factory(&cfg("PLC2", Some(path))).expect("driver");
1466 write(one.as_mut(), "Bench.Count", json!(5));
1467 assert_eq!(value(two.as_mut(), "Bench.Count"), Value::DInt(5));
1468 assert_ne!(value(other.as_mut(), "Bench.Count"), Value::DInt(5));
1469 let plc1 = Arc::clone(&lock(&states)["PLC1"]);
1470 plc1.fail.store(true, Ordering::SeqCst);
1471 assert!(two.open().is_err());
1472 assert!(two.read(&["Bench.Count"]).is_err());
1473 assert!(two.write("Bench.Count", &Value::LInt(1)).is_err());
1474 assert!(other.open().is_ok());
1475 assert_eq!(read(other.as_mut(), "Bench.Count").error, None);
1476 plc1.fail.store(false, Ordering::SeqCst);
1477 assert!(two.open().is_ok());
1478 assert_eq!(value(two.as_mut(), "Bench.Count"), Value::DInt(5));
1479 plc1.version.store(7, Ordering::SeqCst);
1480 assert_eq!(two.version(), json!(7));
1481 }
1482
1483 #[test]
1484 fn bad_files_fail_to_load() {
1485 let recursive = json!({"types": {"U": {"Me": "U"}}, "tags": {"A": "U"}});
1486 for data in [json!([1]), json!({"tags": {"A": "REAL[x]"}}), recursive] {
1487 assert!(
1488 MockPlc::new(&cfg("P", Some(mock_file(&data))), Arc::default()).is_err(),
1489 "{data}"
1490 );
1491 }
1492 assert!(
1493 MockPlc::new(
1494 &cfg("P", Some(PathBuf::from("no/such/file.json"))),
1495 Arc::default()
1496 )
1497 .is_err()
1498 );
1499 }
1500}