Skip to main content

ciplink_server/
mock.rs

1//! A fake driver for `--mock`: serves the tags of the PLC's `mock_tags` JSON file with simulated values.
2//!
3//! The file is the old flat `{name: TYPE}` or `{"types": {UDT: {member: TYPE}}, "tags": {name: TYPE}}`, TYPE
4//! being `NAME` or `NAME[n]` (1-D). Values are functions of the path and the clock, shaped by the name; written
5//! values hold. BYTE, WORD, DWORD and LWORD are bit strings as in ciplink: a list of BOOLs, least significant
6//! first.
7
8use 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/// What the drivers of one PLC share: written values, `fail` (open, read and write fail like a lost connection),
25/// a settable clock (seconds; None = real time) and the tag table version.
26#[derive(Debug)]
27pub struct MockState {
28    /// Written values by canonical leaf path, lowercase.
29    pub written: Mutex<HashMap<String, Value>>,
30    /// Open, read and write fail like a lost connection.
31    pub fail: AtomicBool,
32    /// The clock in seconds since the epoch; None reads the real time.
33    pub clock: Mutex<Option<f64>>,
34    /// The tag table version.
35    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    /// Sets the clock (seconds); None goes back to real time.
51    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
72/// The [`MockState`] of each PLC by name.
73pub type MockStates = Arc<Mutex<HashMap<String, Arc<MockState>>>>;
74
75/// A factory for the server; its drivers share a MockState per PLC name, kept in the returned map for tests.
76pub 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// ------------------------------------------------------------------------------------------------ definitions
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89enum Ty {
90    Atomic,
91    Struct,
92    Str,
93    /// A type the file does not define: listed, but reads and writes fail.
94    Unknown,
95}
96
97/// A tag or member: its type and element count (0 for a scalar).
98#[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    // invariant: the patterns are constants that compile
113    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
149/// The key of `map` that is `name` ignoring case.
150fn 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
158/// Builds the UDTs the tags use, in the layout mock.py computes.
159struct 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
229// ------------------------------------------------------------------------------------------------ types
230
231fn 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
245/// Bits of a bit string type.
246fn 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
260/// `v` wrapped into the range of an integer or bit string type, as that type's value.
261fn 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    // `w` is in the type's range, so the casts keep it
271    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
283/// The bits of an integer (two's complement) or a bit string.
284fn 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
311/// `value` as stored for a leaf of `type_name`, or a `bad_value` error if it does not fit.
312fn 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        // as ciplink: exactly one BOOL (or 0/1) per bit
322        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        // a REAL refuses what does not fit a float32, as ciplink's packing does
334        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
349// ------------------------------------------------------------------------------------------------ the driver
350
351/// The mock driver of one PLC.
352pub struct MockPlc {
353    tags: IndexMap<String, Def>,
354    udts: IndexMap<String, Udt>,
355    state: Arc<MockState>,
356}
357
358/// A request name resolved against the tags.
359struct Resolved<'a> {
360    /// The canonical path.
361    path: String,
362    def: &'a Def,
363    /// A whole array (no index).
364    array: bool,
365    bit: Option<u64>,
366    count: Option<u64>,
367    /// (array path, start) of a range.
368    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    /// The driver of a PLC, serving its `mock_tags` file (no tags without one).
400    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    /// The state this driver shares.
440    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                // an array without an index reads its first element
563                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    /// The value at `path`: a list for a whole array, members for a struct, else the simulated leaf.
592    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    /// A written value, else a deterministic function of the path and time, shaped by the member name.
618    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            // a few toggling bits
666            (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    /// Its setpoint `<member>SP` while the sibling `Running` is true, 0 while it is false; None if there are no
679    /// such siblings.
680    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        // a ripple under a typical deadband
697        Ok(Some(if target != 0.0 {
698            target + 0.2 * now.sin()
699        } else {
700            target
701        }))
702    }
703
704    /// [(leaf path, value)] for writing `value` at `path`. As ciplink: an array written by name takes one value
705    /// (element 0), an array member of a written struct takes all its elements, and a struct takes all its
706    /// members.
707    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                // a bit string's value is itself a list
735                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                    // member names are case-sensitive here
760                    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(&current).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) // back to signed for a signed type
814            };
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)); // a type the file does not define
1074        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]); // no index: the first element
1190        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        // deterministic: another driver of the same state at the same time reads the same
1260        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            // as ciplink: every member (case-sensitive), all of a member array, one value by name, a REAL that
1302            // fits a float32
1303            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()); // nothing of a rejected write is kept
1307        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))); // a few toggling bits
1405        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        ); // not an integer
1446        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}