Skip to main content

ciplink_server/
planner.rs

1//! Read planner: one read of a struct instead of several reads of its members, which are sliced out of it.
2//!
3//! A poll costs about 12 ms per packet plus 1.35 ms per read service, so fewer reads matter, but an extra packet
4//! costs far more than a few reads. For the tags of one cycle, a struct (a tag, an array element or a nested
5//! member, at most `whole_max_bytes`) or a run of elements of one atomic array (`A[3]{5}`) replaces the reads of
6//! the tags under it when that pays: groups are taken best first by extra reply bytes per read saved, each only if
7//! it costs at most [`BYTES_PER_READ`] extra reply bytes per read saved and the cycle's estimated packet count does
8//! not grow over plain reads. A `together` struct is always read whole.
9
10use std::cmp::Ordering;
11use std::collections::{BTreeSet, HashMap, HashSet};
12
13use ciplink::Value;
14use ciplink::cip::TagResult;
15use indexmap::IndexMap;
16
17use crate::plc::{Catalog, Kind, Node, Plc, atomic_size};
18
19/// One read service (~1.35 ms) costs about as much packet time as this many reply bytes.
20pub const BYTES_PER_READ: u64 = 450;
21/// Request and reply budget of a packet when the driver has no connection size.
22pub const PACKET_BYTES: usize = 4000;
23/// Request bytes of ciplink's tag table version check, sent in every packet with instance IDs.
24pub const VERSION_REQUEST: usize = 20;
25/// Reply bytes of ciplink's tag table version check.
26pub const VERSION_REPLY: usize = 44;
27
28/// A slice step into a read value.
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
30pub enum Key {
31    /// A struct member (matched ignoring case).
32    Member(String),
33    /// An array element.
34    Index(usize),
35}
36
37/// One read service of a plan and the requested tags it serves.
38#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
39pub struct Read {
40    /// The tag name read: a tag, a struct (`Line1.Motor`) or a run (`Trend[3]{5}`).
41    pub name: String,
42    /// `(tag, path)`: the keys that slice the tag's value out of the read value, None for the value as read.
43    pub members: Vec<(String, Option<Vec<Key>>)>,
44}
45
46// ------------------------------------------------------------------------------------------------ names
47
48/// What a token of a tag name addresses.
49#[derive(Clone, Debug, PartialEq, Eq)]
50enum Tok {
51    Name(String),
52    Index(usize),
53    /// A multi-dimensional index; never sliced through.
54    Multi(Vec<usize>),
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
58struct Token {
59    text: String,
60    key: Tok,
61}
62
63/// The tokens of a tag name; None for what is never grouped: bit access (`X.5`), `X{3}`, odd syntax.
64fn tokenize(name: &str) -> Option<Vec<Token>> {
65    let (prefix, rest) = match name.get(..8) {
66        Some(head) if head.eq_ignore_ascii_case("program:") => {
67            let dot = name.find('.')?;
68            (&name[..=dot], &name[dot + 1..])
69        }
70        _ => ("", name),
71    };
72    let bytes = rest.as_bytes();
73    let mut tokens: Vec<Token> = Vec::new();
74    let mut pos = 0;
75    while pos < bytes.len() {
76        let start = pos;
77        if bytes[pos] == b'[' {
78            let end = pos + rest[pos..].find(']')?;
79            let index = rest[pos + 1..end]
80                .split(',')
81                .map(|i| {
82                    if i.is_empty() || !i.bytes().all(|b| b.is_ascii_digit()) {
83                        None
84                    } else {
85                        i.parse::<usize>().ok()
86                    }
87                })
88                .collect::<Option<Vec<_>>>()?;
89            pos = end + 1;
90            let key = match index.as_slice() {
91                [one] => Tok::Index(*one),
92                _ => Tok::Multi(index),
93            };
94            tokens.push(Token {
95                text: rest[start..pos].to_owned(),
96                key,
97            });
98            continue;
99        }
100        let dotted = bytes[pos] == b'.';
101        if dotted != (pos > 0) {
102            return None; // the first name has no dot, the others need one
103        }
104        if dotted {
105            pos += 1;
106        }
107        if !bytes
108            .get(pos)
109            .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
110        {
111            return None;
112        }
113        let name_start = pos;
114        while bytes
115            .get(pos)
116            .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
117        {
118            pos += 1;
119        }
120        tokens.push(Token {
121            text: rest[start..pos].to_owned(),
122            key: Tok::Name(rest[name_start..pos].to_owned()),
123        });
124    }
125    let first = tokens.first_mut()?;
126    let Tok::Name(base) = &first.key else {
127        return None;
128    };
129    first.key = Tok::Name(format!("{prefix}{base}"));
130    first.text = format!("{prefix}{}", first.text);
131    Some(tokens)
132}
133
134fn join(tokens: &[Token]) -> String {
135    tokens.iter().map(|t| t.text.as_str()).collect()
136}
137
138fn to_usize(v: u64) -> usize {
139    usize::try_from(v).unwrap_or(usize::MAX)
140}
141
142fn padded(text: &str) -> usize {
143    2 + text.len() + text.len() % 2
144}
145
146fn index_size(i: usize) -> usize {
147    if i < 256 { 2 } else { 4 }
148}
149
150/// A BOOL array: its elements are bits, never sliced through or read as a run.
151fn bools(node: &Node) -> bool {
152    node.bool_array() || node.type_name == "BOOL"
153}
154
155// ------------------------------------------------------------------------------------------------ sizes
156
157/// Estimated size of a Read Tag service for `name`, the base tag addressed by its instance ID (a 16-bit ID when
158/// unknown, as on most real controllers).
159fn request_size(name: &str, catalog: &Catalog) -> usize {
160    let Some(tokens) = tokenize(name) else {
161        return 10 + name.len() + name.len() % 2;
162    };
163    let base = &tokens[0].text;
164    let instance = catalog.info(base).and_then(|n| n.instance_id);
165    let mut size = 8 + match instance {
166        Some(i) if i < 256 => 2,
167        Some(i) if i >= 65536 => 6,
168        _ => 4,
169    };
170    if base
171        .get(..8)
172        .is_some_and(|h| h.eq_ignore_ascii_case("program:"))
173    {
174        size += padded(base.split('.').next().unwrap_or_default());
175    }
176    for token in &tokens[1..] {
177        size += match &token.key {
178            Tok::Name(n) => padded(n),
179            Tok::Index(i) => index_size(*i),
180            Tok::Multi(v) => v.iter().map(|i| index_size(*i)).sum(),
181        };
182    }
183    size
184}
185
186/// Estimated reply size of a read of one element of `node` (12 when unknown).
187fn reply_size(node: Option<&Node>) -> usize {
188    match node {
189        None => 12,
190        Some(n) if n.kind != Kind::Atomic => {
191            to_usize(n.structure_size.unwrap_or(4)).saturating_add(10)
192        }
193        Some(n) => to_usize(atomic_size(&n.type_name).unwrap_or(4)) + 8,
194    }
195}
196
197/// `A[3]{5}` as `("A[3]", 5)`.
198fn range(name: &str) -> Option<(&str, usize)> {
199    let digits = name.strip_suffix('}')?;
200    let brace = digits.rfind('{')?;
201    let (base, count) = (&digits[..brace], &digits[brace + 1..]);
202    if base.is_empty() || count.is_empty() || !count.bytes().all(|b| b.is_ascii_digit()) {
203        return None;
204    }
205    Some((base, count.parse().ok()?))
206}
207
208/// Estimated (request, reply) sizes of a read of `name` as [`plan`] writes it, `A[3]{5}` included.
209fn read_size(name: &str, catalog: &Catalog) -> (usize, usize) {
210    let Some((base, count)) = range(name) else {
211        return (request_size(name, catalog), reply_size(catalog.info(name)));
212    };
213    let array = catalog.info(base);
214    let request = request_size(base, catalog);
215    if array.is_some_and(bools) {
216        return (request, count.div_ceil(32) * 4 + 8); // BOOLs, 32 per word
217    }
218    let overhead = if array.is_some_and(|a| a.kind != Kind::Atomic) {
219        10
220    } else {
221        8
222    };
223    let element = reply_size(array).saturating_sub(overhead);
224    (
225        request,
226        count.saturating_mul(element).saturating_add(overhead),
227    )
228}
229
230/// Size of one element of an atomic 1-D array whose elements can be read as a run, else None. Elements of
231/// BOOL arrays and of bit strings (BYTE, WORD, DWORD, LWORD) are bits and never run.
232fn element_size(node: Option<&Node>) -> Option<usize> {
233    let node = node?;
234    if node.kind != Kind::Atomic || node.dims.len() != 1 {
235        return None;
236    }
237    match node.type_name.as_str() {
238        "BOOL" | "BYTE" | "WORD" | "DWORD" | "LWORD" => None,
239        t => atomic_size(t).map(to_usize),
240    }
241}
242
243/// The structure size if the path is one struct that can be read whole, else None.
244fn struct_size(catalog: &Catalog, node: Option<&Node>, last: &Tok) -> Option<u64> {
245    let node = node?;
246    if matches!(last, Tok::Name(_)) && !node.dims.is_empty() {
247        return None; // a whole array: a read without an index gets its first element only
248    }
249    catalog.struct_size(node)
250}
251
252// ------------------------------------------------------------------------------------------------ packets
253
254/// The indexes of the reads in each packet and the packet count, packed in order as ciplink does: the other
255/// reads first, then each fragmented read alone, taking several packets. A read sent alone also gets a
256/// standalone version check before and after it (the worker sends each in its own plc.read, so both count).
257fn pack(sizes: &[(usize, usize)], budget: usize) -> (Vec<Vec<usize>>, usize) {
258    let (empty_request, empty_reply) = (10 + VERSION_REQUEST, 8 + VERSION_REPLY);
259    let mut groups = Vec::new();
260    let mut fragmented = Vec::new();
261    let mut count = 0;
262    let mut current = Vec::new();
263    let (mut request, mut reply) = (empty_request, empty_reply);
264    for (i, &(req, rep)) in sizes.iter().enumerate() {
265        if empty_request.saturating_add(req) > budget || empty_reply.saturating_add(rep) > budget {
266            fragmented.push(vec![i]);
267            count += rep
268                .div_ceil(budget.saturating_sub(empty_reply).max(1))
269                .max(1)
270                + 2;
271            continue;
272        }
273        if !current.is_empty() && (request + req > budget || reply + rep > budget) {
274            groups.push(std::mem::take(&mut current));
275            count += 1;
276            (request, reply) = (empty_request, empty_reply);
277        }
278        current.push(i);
279        request += req;
280        reply += rep;
281    }
282    if !current.is_empty() {
283        groups.push(current);
284        count += 1;
285    }
286    groups.extend(fragmented);
287    (groups, count)
288}
289
290/// Packets for reads of these (request, reply) sizes, packed in order as ciplink does.
291pub fn packets(sizes: &[(usize, usize)], budget: usize) -> usize {
292    pack(sizes, budget).1
293}
294
295// ------------------------------------------------------------------------------------------------ plan
296
297/// A read while planning: its group key (None for a plain read), the tags it serves, sizes and name.
298#[derive(Clone)]
299struct Pending {
300    key: Option<String>,
301    members: Vec<String>,
302    size: (usize, usize),
303    name: String,
304}
305
306/// A run of requested elements of one atomic array.
307struct Run<'a> {
308    array: String,
309    size: usize,
310    index: HashMap<&'a str, usize>,
311}
312
313/// A merge of the reads under one key.
314struct Merged {
315    reads: Vec<Pending>,
316    saved: usize,
317    extra: i128,
318    name: String,
319}
320
321/// The reads for `tags` in packet order. `catalog` holds the tag definitions; names in `no_group` (lowercase)
322/// are not read whole; `budget` is bytes per packet; `together(struct path)` is true if the requested tags under
323/// that struct are always read in one read.
324pub fn plan(
325    tags: &[String],
326    catalog: &Catalog,
327    whole_max_bytes: u64,
328    no_group: &HashSet<String>,
329    budget: usize,
330    together: Option<&dyn Fn(&str) -> bool>,
331) -> Vec<Read> {
332    let mut parsed: HashMap<&str, Vec<Token>> = HashMap::new();
333    let mut ancestors: Vec<(&str, Vec<String>)> = Vec::new(); // structs above a tag that read whole, deepest first
334    let mut depth: HashMap<&str, HashMap<String, usize>> = HashMap::new(); // tag -> key -> tokens of that struct
335    let mut names: HashMap<String, String> = HashMap::new(); // struct key -> read name
336    let mut sizes: IndexMap<String, u64> = IndexMap::new(); // struct key -> structure size
337    let mut levels: HashMap<String, usize> = HashMap::new(); // struct key -> number of tokens
338    let mut whole: HashMap<String, &str> = HashMap::new(); // key -> requested tag that is itself that struct
339    let mut elements: IndexMap<String, Vec<(usize, &str)>> = IndexMap::new(); // array key -> requested elements
340    let mut arrays: HashMap<String, (String, usize)> = HashMap::new(); // array key -> (name, element size)
341    let mut seen = HashSet::new();
342    for tag in tags {
343        let Some(tokens) = tokenize(tag) else {
344            continue;
345        };
346        if !seen.insert(tag.to_lowercase()) {
347            continue;
348        }
349        let n = tokens.len();
350        let last = &tokens[n - 1].key;
351        let Some(info) = catalog.info(tag) else {
352            continue;
353        };
354        if !info.dims.is_empty() && matches!(last, Tok::Name(_)) {
355            continue; // an array read without an index (its first element only)
356        }
357        if let (true, Tok::Index(i)) = (n > 1, last) {
358            let array = join(&tokens[..n - 1]);
359            let akey = array.to_lowercase();
360            if let Some(size) = element_size(catalog.info(&array)) {
361                if !no_group.contains(&akey) {
362                    elements
363                        .entry(akey.clone())
364                        .or_default()
365                        .push((*i, tag.as_str()));
366                    arrays.insert(akey, (array, size));
367                }
368            }
369        }
370        let mut found = Vec::new();
371        for k in (1..=n).rev() {
372            if k < n {
373                // the slice gains token k: a name, or an index of an array that is not BOOLs
374                match &tokens[k].key {
375                    Tok::Multi(_) => break,
376                    Tok::Index(_) => {
377                        if !catalog.info(&join(&tokens[..k])).is_some_and(|a| !bools(a)) {
378                            break;
379                        }
380                    }
381                    Tok::Name(_) => {}
382                }
383            }
384            let prefix = join(&tokens[..k]);
385            let key = prefix.to_lowercase();
386            if no_group.contains(&key) {
387                continue;
388            }
389            let Some(size) = struct_size(catalog, catalog.info(&prefix), &tokens[k - 1].key) else {
390                continue;
391            };
392            depth
393                .entry(tag.as_str())
394                .or_default()
395                .insert(key.clone(), k);
396            names.entry(key.clone()).or_insert(prefix);
397            sizes.insert(key.clone(), size);
398            levels.insert(key.clone(), k);
399            if k == n {
400                whole.insert(key, tag.as_str());
401            } else {
402                found.push(key);
403            }
404        }
405        ancestors.push((tag.as_str(), found));
406        parsed.insert(tag.as_str(), tokens);
407    }
408
409    let mut under: HashMap<String, HashSet<&str>> = HashMap::new(); // key -> the tags it can serve
410    for (tag, found) in &ancestors {
411        for key in found {
412            under.entry(key.clone()).or_default().insert(tag);
413        }
414    }
415    for (key, tag) in &whole {
416        under.entry(key.clone()).or_default().insert(tag);
417    }
418    let mut candidates: BTreeSet<String> = under
419        .iter()
420        .filter(|(key, served)| {
421            served.len() >= 2
422                && (whole.contains_key(*key)
423                    || sizes.get(*key).is_some_and(|s| *s <= whole_max_bytes))
424        })
425        .map(|(key, _)| key.clone())
426        .collect();
427
428    // array runs: the requested elements, split where bridging the gap alone would break the cost rule
429    let mut runs: HashMap<String, Run> = HashMap::new();
430    for (akey, items) in &mut elements {
431        items.sort_unstable();
432        let (array, size) = &arrays[akey];
433        let mut clusters: Vec<Vec<(usize, &str)>> = Vec::new();
434        for &(index, tag) in items.iter() {
435            let gap = clusters.last().and_then(|c| c.last()).map(|&(prev, _)| {
436                index
437                    .saturating_sub(prev)
438                    .saturating_sub(1)
439                    .saturating_mul(*size)
440            });
441            if gap.is_none_or(|g| g as u64 > BYTES_PER_READ) {
442                clusters.push(Vec::new());
443            }
444            if let Some(c) = clusters.last_mut() {
445                c.push((index, tag));
446            }
447        }
448        for (c, cluster) in clusters.iter().enumerate() {
449            if cluster.len() >= 2 {
450                let key = format!("{akey}{{{c}"); // a "{" is never in a struct key
451                under.insert(key.clone(), cluster.iter().map(|&(_, t)| t).collect());
452                runs.insert(
453                    key.clone(),
454                    Run {
455                        array: array.clone(),
456                        size: *size,
457                        index: cluster.iter().map(|&(i, t)| (t, i)).collect(),
458                    },
459                );
460                candidates.insert(key);
461            }
462        }
463    }
464
465    let start_of = |run: &Run, members: &[String]| -> (usize, usize) {
466        let indexes = members
467            .iter()
468            .filter_map(|t| run.index.get(t.as_str()).copied());
469        let start = indexes.clone().min().unwrap_or(0);
470        (start, indexes.max().unwrap_or(start) - start + 1)
471    };
472    // the read name and sizes of the read of `key` that serves `members`
473    let group = |key: &str, members: &[String]| -> (String, (usize, usize)) {
474        match runs.get(key) {
475            None => {
476                let name = names.get(key).cloned().unwrap_or_default();
477                let reply = to_usize(sizes.get(key).copied().unwrap_or(0)).saturating_add(10);
478                let request = request_size(&name, catalog);
479                (name, (request, reply))
480            }
481            Some(run) => {
482                let (start, count) = start_of(run, members);
483                let element = format!("{}[{start}]", run.array);
484                let reply = count.saturating_mul(run.size).saturating_add(8);
485                (
486                    format!("{element}{{{count}}}"),
487                    (request_size(&element, catalog), reply),
488                )
489            }
490        }
491    };
492    // the reads with the ones all under `key` merged at the first one's place, None if fewer than two
493    let merge = |key: &str, reads: &[Pending]| -> Option<Merged> {
494        let served = under.get(key)?;
495        let covered: Vec<usize> = (0..reads.len())
496            .filter(|&i| reads[i].members.iter().all(|t| served.contains(t.as_str())))
497            .collect();
498        if covered.len() < 2 {
499            return None;
500        }
501        let members: Vec<String> = covered
502            .iter()
503            .flat_map(|&i| reads[i].members.iter().cloned())
504            .collect();
505        let (name, size) = group(key, &members);
506        let before: usize = covered.iter().map(|&i| reads[i].size.1).sum();
507        let extra = size.1 as i128 - before as i128;
508        let mut new = Vec::with_capacity(reads.len() + 1 - covered.len());
509        for (i, r) in reads.iter().enumerate() {
510            if i == covered[0] {
511                new.push(Pending {
512                    key: Some(key.to_owned()),
513                    members: members.clone(),
514                    size,
515                    name: name.clone(),
516                });
517            } else if !covered.contains(&i) {
518                new.push(r.clone());
519            }
520        }
521        Some(Merged {
522            reads: new,
523            saved: covered.len() - 1,
524            extra,
525            name,
526        })
527    };
528    let sizes_of =
529        |reads: &[Pending]| -> Vec<(usize, usize)> { reads.iter().map(|r| r.size).collect() };
530
531    // start plain, sized as chunks() sizes them
532    let mut reads: Vec<Pending> = tags
533        .iter()
534        .map(|tag| Pending {
535            key: None,
536            members: vec![tag.clone()],
537            size: read_size(tag, catalog),
538            name: tag.clone(),
539        })
540        .collect();
541    if let Some(together) = together {
542        // outermost first; ignores whole_max_bytes and the cost rule, but must fit one packet
543        let fits = budget.saturating_sub(8 + VERSION_REPLY);
544        let mut keys: Vec<&String> = sizes
545            .iter()
546            .filter(|(k, s)| {
547                under.get(*k).is_some_and(|u| u.len() >= 2)
548                    && to_usize(**s).saturating_add(10) <= fits
549            })
550            .map(|(k, _)| k)
551            .collect();
552        keys.sort_by_key(|k| levels.get(*k).copied().unwrap_or(0)); // stable: upload order within a level
553        for key in keys {
554            if together(names.get(key).map_or("", String::as_str)) {
555                if let Some(merged) = merge(key, &reads) {
556                    reads = merged.reads;
557                }
558                candidates.remove(key);
559            }
560        }
561    }
562    let limit = packets(&sizes_of(&reads), budget);
563    loop {
564        // best: lowest extra bytes per read saved, then most reads saved, then key
565        let mut best: Option<(String, Merged)> = None;
566        for key in &candidates {
567            let Some(merged) = merge(key, &reads) else {
568                continue;
569            };
570            if merged.extra > i128::from(BYTES_PER_READ) * merged.saved as i128
571                || no_group.contains(&merged.name.to_lowercase())
572                || packets(&sizes_of(&merged.reads), budget) > limit
573            {
574                continue;
575            }
576            let better = best.as_ref().is_none_or(|(_, b)| {
577                (merged.extra * b.saved as i128)
578                    .cmp(&(b.extra * merged.saved as i128))
579                    .then(b.saved.cmp(&merged.saved))
580                    == Ordering::Less
581            });
582            if better {
583                best = Some((key.clone(), merged));
584            }
585        }
586        let Some((key, merged)) = best else { break };
587        reads = merged.reads;
588        candidates.remove(&key);
589    }
590
591    reads
592        .into_iter()
593        .map(|r| {
594            let members = match &r.key {
595                None => vec![(r.name.clone(), None)],
596                Some(key) => match runs.get(key) {
597                    Some(run) => {
598                        let (start, _) = start_of(run, &r.members);
599                        r.members
600                            .iter()
601                            .map(|t| {
602                                let i = run.index.get(t.as_str()).copied().unwrap_or(start);
603                                (t.clone(), Some(vec![Key::Index(i - start)]))
604                            })
605                            .collect()
606                    }
607                    None => r
608                        .members
609                        .iter()
610                        .map(|t| {
611                            let tokens = parsed.get(t.as_str()).map_or(&[][..], Vec::as_slice);
612                            let k = depth
613                                .get(t.as_str())
614                                .and_then(|d| d.get(key))
615                                .copied()
616                                .unwrap_or(tokens.len());
617                            let path = (k < tokens.len()).then(|| {
618                                tokens[k..]
619                                    .iter()
620                                    .map(|tok| match &tok.key {
621                                        Tok::Name(n) => Key::Member(n.clone()),
622                                        Tok::Index(i) => Key::Index(*i),
623                                        // never in a path: plan() stops slicing at a multi-dimensional index
624                                        Tok::Multi(v) => {
625                                            Key::Index(v.first().copied().unwrap_or(0))
626                                        }
627                                    })
628                                    .collect()
629                            });
630                            (t.clone(), path)
631                        })
632                        .collect(),
633                },
634            };
635            Read {
636                name: r.name,
637                members,
638            }
639        })
640        .collect()
641}
642
643/// `reads` (a plan) split into lists of about one packet each, packed as [`packets`] counts them.
644pub fn chunks(reads: Vec<Read>, catalog: &Catalog, budget: usize) -> Vec<Vec<Read>> {
645    let sizes: Vec<_> = reads.iter().map(|r| read_size(&r.name, catalog)).collect();
646    let (groups, _) = pack(&sizes, budget);
647    let mut slots: Vec<Option<Read>> = reads.into_iter().map(Some).collect();
648    groups
649        .into_iter()
650        .map(|g| {
651            g.into_iter()
652                .filter_map(|i| slots.get_mut(i).and_then(Option::take))
653                .collect()
654        })
655        .collect()
656}
657
658/// The part of a read value at `path` (member names ignoring case, array indexes), None if it isn't there.
659pub fn dig(value: &Value, path: &[Key]) -> Option<Value> {
660    let mut value = value;
661    for key in path {
662        value = match (key, value) {
663            (Key::Index(i), Value::Array(items)) => items.get(*i)?,
664            (Key::Member(m), Value::Struct(members)) => members.get(m).or_else(|| {
665                members
666                    .iter()
667                    .find(|(k, _)| k.eq_ignore_ascii_case(m))
668                    .map(|(_, v)| v)
669            })?,
670            _ => return None,
671        };
672    }
673    Some(value.clone())
674}
675
676/// Runs a plan split in chunks, one `plc.read` per chunk; `between(plc, partial results)` runs after every chunk
677/// but the last (the worker publishes those results and runs queued writes there), and its results are left out
678/// of the returned ones. Returns (results by tag, names of failed group reads, read services sent). The members
679/// of failed group reads are read one by one after the last chunk, so no value is ever wrong. Err = a lost
680/// connection from `plc.read`, or an error from `between`.
681#[allow(clippy::type_complexity)]
682pub fn execute(
683    plc: &mut dyn Plc,
684    chunks: &[Vec<Read>],
685    between: &mut dyn FnMut(&mut dyn Plc, HashMap<String, TagResult>) -> Result<(), String>,
686) -> Result<(HashMap<String, TagResult>, Vec<String>, usize), String> {
687    let mut out = HashMap::new();
688    let mut retry: Vec<String> = Vec::new();
689    let mut failed: Vec<String> = Vec::new();
690    let mut services = 0;
691    for (n, chunk) in chunks.iter().enumerate() {
692        let names: Vec<&str> = chunk.iter().map(|r| r.name.as_str()).collect();
693        let results = if names.is_empty() {
694            Vec::new()
695        } else {
696            plc.read(&names)?
697        };
698        services += names.len();
699        let mut part = HashMap::new();
700        for (read, result) in chunk.iter().zip(results) {
701            if let [(tag, None)] = read.members.as_slice() {
702                part.insert(tag.clone(), result);
703                continue;
704            }
705            if result.error.is_some() {
706                failed.push(read.name.clone());
707                retry.extend(read.members.iter().map(|(t, _)| t.clone()));
708                continue;
709            }
710            for (tag, path) in &read.members {
711                let Some(path) = path else {
712                    part.insert(tag.clone(), result.clone());
713                    continue;
714                };
715                match result.value.as_ref().and_then(|v| dig(v, path)) {
716                    Some(value) => {
717                        let sliced = TagResult {
718                            tag: tag.clone(),
719                            value: Some(value),
720                            data_type: None,
721                            error: None,
722                        };
723                        part.insert(tag.clone(), sliced);
724                    }
725                    None => {
726                        if !failed.contains(&read.name) {
727                            failed.push(read.name.clone());
728                        }
729                        retry.push(tag.clone());
730                    }
731                }
732            }
733        }
734        if n + 1 < chunks.len() {
735            between(&mut *plc, part)?;
736        } else {
737            out.extend(part);
738        }
739    }
740    if !retry.is_empty() {
741        let names: Vec<&str> = retry.iter().map(String::as_str).collect();
742        let results = plc.read(&names)?;
743        out.extend(retry.iter().cloned().zip(results));
744    }
745    Ok((out, failed, services + retry.len()))
746}
747
748#[cfg(test)]
749mod tests {
750    //! Ports of ciplink-tagserver's tests/test_planner.py, on the same fake controller (its conftest.py).
751
752    use std::cell::RefCell;
753    use std::collections::BTreeMap;
754    use std::sync::{Arc, Mutex};
755
756    use ciplink::{ErrorKind, TagError};
757    use regex::Regex;
758    use serde_json::Value as Json;
759
760    use super::*;
761    use crate::plc::{Aoi, TypeDef};
762
763    // -------------------------------------------------------------------------------------------- catalog
764
765    fn atomic(t: &str, dims: &[u64]) -> Node {
766        Node {
767            type_name: t.into(),
768            kind: Kind::Atomic,
769            dims: dims.to_vec(),
770            member: false,
771            instance_id: None,
772            structure_size: None,
773        }
774    }
775
776    fn strct(t: &str, size: u64, dims: &[u64]) -> Node {
777        Node {
778            kind: Kind::Struct,
779            structure_size: Some(size),
780            ..atomic(t, dims)
781        }
782    }
783
784    fn udt(size: u64, members: Vec<(&str, Node)>, whole_read: Option<bool>) -> TypeDef {
785        TypeDef {
786            public: members.iter().map(|(m, _)| (*m).to_owned()).collect(),
787            members: members
788                .into_iter()
789                .map(|(m, n)| (m.to_owned(), Node { member: true, ..n }))
790                .collect(),
791            structure_size: size,
792            aoi: whole_read.map(|whole_read| Aoi {
793                whole_read,
794                readable: Vec::new(),
795            }),
796            whole_readable: true,
797        }
798    }
799
800    fn dints(size: u64) -> TypeDef {
801        udt(
802            size,
803            vec![("A", atomic("DINT", &[])), ("B", atomic("DINT", &[]))],
804            None,
805        )
806    }
807
808    fn base_types() -> Vec<(&'static str, TypeDef)> {
809        let bools = |names: &[&'static str]| {
810            names
811                .iter()
812                .map(|n| (*n, atomic("BOOL", &[])))
813                .collect::<Vec<_>>()
814        };
815        let valve = bools(&["EnableIn", "EnableOut", "Open", "Cmd"]);
816        let mut pump = bools(&["EnableIn", "EnableOut", "Run"]);
817        pump.push(("Speed", atomic("REAL", &[])));
818        let real = || atomic("REAL", &[]);
819        let dint = || atomic("DINT", &[]);
820        let motor = vec![
821            ("Speed", real()),
822            ("Current", real()),
823            ("Running", atomic("BOOL", &[])),
824            ("Count", dint()),
825        ];
826        let station = vec![
827            ("Motor", strct("Motor", 40, &[])),
828            ("Ready", atomic("BOOL", &[])),
829            ("Data", atomic("DINT", &[4])),
830            ("Bits", atomic("DWORD", &[1])), // a BOOL[32] member
831        ];
832        // Cell holds an AOI that can't be read whole
833        let cell = vec![
834            ("Valve", strct("Valve_AOI", 60, &[])),
835            ("Mode", dint()),
836            ("Step", dint()),
837        ];
838        let tank = vec![
839            ("Level", real()),
840            ("Temp", real()),
841            ("Valve", atomic("BOOL", &[])),
842        ];
843        vec![
844            ("Motor", udt(40, motor, None)),
845            ("Station", udt(200, station, None)),
846            ("Big", dints(4000)),
847            ("Valve_AOI", udt(60, valve, Some(false))),
848            ("Pump_AOI", udt(40, pump, Some(true))),
849            ("Cell", udt(300, cell, None)),
850            ("Tank", udt(12, tank, None)),
851        ]
852    }
853
854    fn base_tags() -> Vec<(&'static str, Node)> {
855        let string = Node {
856            kind: Kind::String,
857            ..strct("STRING", 88, &[])
858        };
859        vec![
860            ("Line1", strct("Station", 200, &[])),
861            ("Line2", strct("Station", 200, &[])),
862            ("Arr", strct("Station", 200, &[5])),
863            ("Program:Station1.Motor", strct("Motor", 40, &[])),
864            ("Big1", strct("Big", 4000, &[])),
865            ("Valve1", strct("Valve_AOI", 60, &[])),
866            ("Pump1", strct("Pump_AOI", 40, &[])),
867            ("Cell1", strct("Cell", 300, &[])),
868            ("Name1", string),
869            ("Speed", atomic("REAL", &[])),
870            ("Count", atomic("DINT", &[])),
871            ("Flags", atomic("DINT", &[])),
872            ("Trend", atomic("REAL", &[16])),
873            ("Long", atomic("REAL", &[1000])),
874            ("BoolArr", atomic("DWORD", &[32])), // ciplink's BOOL[32]
875            ("Tank", strct("Tank", 12, &[4])),
876            ("Grid", atomic("DINT", &[4, 4])),
877        ]
878    }
879
880    fn catalog_with(tags: Vec<(&str, Node)>, types: Vec<(&str, TypeDef)>) -> Catalog {
881        let tags = base_tags()
882            .into_iter()
883            .chain(tags)
884            .map(|(k, v)| (k.to_owned(), v))
885            .collect();
886        let types = base_types()
887            .into_iter()
888            .chain(types)
889            .map(|(k, v)| (k.to_owned(), v))
890            .collect();
891        Catalog::new(Json::Null, tags, types)
892    }
893
894    fn catalog() -> Catalog {
895        catalog_with(vec![], vec![])
896    }
897
898    fn blob(size: u64) -> Catalog {
899        catalog_with(
900            vec![("Blob1", strct("Blob", size, &[]))],
901            vec![("Blob", dints(size))],
902        )
903    }
904
905    // -------------------------------------------------------------------------------------------- values
906
907    fn st(members: Vec<(&str, Value)>) -> Value {
908        Value::Struct(
909            members
910                .into_iter()
911                .map(|(k, v)| (k.to_owned(), v))
912                .collect(),
913        )
914    }
915
916    fn motor(speed: f32) -> Value {
917        st(vec![
918            ("Speed", Value::Real(speed)),
919            ("Current", Value::Real(speed / 10.0)),
920            ("Running", Value::Bool(true)),
921            ("Count", Value::DInt(0b101101)),
922        ])
923    }
924
925    fn station(n: i32) -> Value {
926        st(vec![
927            ("Motor", motor(n as f32 * 1.5)),
928            ("Ready", Value::Bool(n % 2 == 0)),
929            ("Data", Value::Array((n..n + 4).map(Value::DInt).collect())),
930            ("Bits", Value::Array(vec![Value::Bool(true); 32])),
931        ])
932    }
933
934    fn values() -> HashMap<String, Value> {
935        let b = Value::Bool;
936        let tank = |i: i32| {
937            st(vec![
938                ("Level", Value::Real(i as f32 * 10.0)),
939                ("Temp", Value::Real(20.0 + i as f32)),
940                ("Valve", b(i % 2 == 0)),
941            ])
942        };
943        let pump = vec![
944            ("EnableIn", b(true)),
945            ("EnableOut", b(true)),
946            ("Run", b(true)),
947            ("Speed", Value::Real(12.5)),
948        ];
949        let cell = vec![
950            (
951                "Valve",
952                st(vec![
953                    ("EnableIn", b(true)),
954                    ("EnableOut", b(false)),
955                    ("Open", b(true)),
956                ]),
957            ),
958            ("Mode", Value::DInt(2)),
959            ("Step", Value::DInt(30)),
960        ];
961        let pairs = vec![
962            ("Line1", station(1)),
963            ("Line2", station(2)),
964            ("Arr", Value::Array((0..5).map(station).collect())),
965            ("Program:Station1.Motor", motor(7.5)),
966            (
967                "Big1",
968                st(vec![("A", Value::DInt(1)), ("B", Value::DInt(2))]),
969            ),
970            // Cmd is local: not readable
971            (
972                "Valve1",
973                st(vec![
974                    ("EnableIn", b(true)),
975                    ("EnableOut", b(true)),
976                    ("Open", b(false)),
977                ]),
978            ),
979            ("Pump1", st(pump)),
980            ("Cell1", st(cell)),
981            ("Name1", Value::String("hello".into())),
982            ("Speed", Value::Real(3.5)),
983            ("Count", Value::DInt(9)),
984            ("Flags", Value::DInt(0b1010)),
985            (
986                "Trend",
987                Value::Array((0..16).map(|i| Value::Real(i as f32 / 2.0)).collect()),
988            ),
989            (
990                "Long",
991                Value::Array((0..1000).map(|i| Value::Real(i as f32)).collect()),
992            ),
993            (
994                "BoolArr",
995                Value::Array((0..32).map(|i| b(i % 3 == 0)).collect()),
996            ),
997            ("Tank", Value::Array((0..4).map(tank).collect())),
998        ];
999        pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect()
1000    }
1001
1002    // -------------------------------------------------------------------------------------------- fake PLC
1003
1004    #[derive(Debug, PartialEq)]
1005    enum Event {
1006        Read(Vec<String>),
1007        Between(Vec<String>),
1008    }
1009
1010    /// The fake driver of conftest.py: values by tag; member paths, bits and ranges are read out of them.
1011    struct Fake {
1012        catalog: Catalog,
1013        values: HashMap<String, Value>,
1014        errors: HashMap<String, String>,
1015        log: Arc<Mutex<Vec<Event>>>,
1016    }
1017
1018    impl Fake {
1019        fn new(catalog: Catalog) -> Self {
1020            Fake {
1021                catalog,
1022                values: values(),
1023                errors: HashMap::new(),
1024                log: Arc::default(),
1025            }
1026        }
1027
1028        fn reads(&self) -> Vec<Vec<String>> {
1029            let log = self.log.lock().expect("log");
1030            log.iter()
1031                .filter_map(|e| match e {
1032                    Event::Read(names) => Some(names.clone()),
1033                    Event::Between(_) => None,
1034                })
1035                .collect()
1036        }
1037
1038        fn value(&self, name: &str) -> Option<Value> {
1039            if let Some(v) = self.values.get(name) {
1040                return Some(v.clone());
1041            }
1042            let (prefix, rest) = if name.to_lowercase().starts_with("program:") {
1043                let (p, r) = name.split_once('.')?;
1044                (format!("{p}."), r)
1045            } else {
1046                (String::new(), name)
1047            };
1048            let re = Regex::new(r"[^.\[\]]+|\[\d+\]").expect("regex");
1049            let parts: Vec<&str> = re.find_iter(rest).map(|m| m.as_str()).collect();
1050            let base = format!("{prefix}{}", parts.first()?);
1051            let mut value = self
1052                .values
1053                .iter()
1054                .find(|(k, _)| k.eq_ignore_ascii_case(&base))
1055                .map(|(_, v)| v.clone())?;
1056            for part in &parts[1..] {
1057                value = if let Some(i) = part.strip_prefix('[') {
1058                    dig(&value, &[Key::Index(i.trim_end_matches(']').parse().ok()?)])?
1059                } else if part.bytes().all(|b| b.is_ascii_digit()) {
1060                    let bit: u32 = part.parse().ok()?;
1061                    match value {
1062                        Value::DInt(v) => Value::Bool(v >> bit & 1 == 1),
1063                        _ => return None,
1064                    }
1065                } else {
1066                    dig(&value, &[Key::Member((*part).to_owned())])?
1067                };
1068            }
1069            Some(value)
1070        }
1071
1072        fn result(&self, tag: &str, range: &Regex) -> TagResult {
1073            let mut result = TagResult {
1074                tag: tag.to_owned(),
1075                value: None,
1076                data_type: None,
1077                error: None,
1078            };
1079            if let Some(e) = self.errors.get(tag) {
1080                result.error = Some(TagError::new(e.clone(), ErrorKind::Device));
1081                return result;
1082            }
1083            let value = match range.captures(tag) {
1084                Some(c) => {
1085                    let start: usize = c.get(2).map_or(0, |m| m.as_str().parse().expect("start"));
1086                    let count: usize = c[3].parse().expect("count");
1087                    match self.value(&c[1]) {
1088                        Some(Value::Array(items)) if items.len() >= start + count => {
1089                            Some(Value::Array(items[start..start + count].to_vec()))
1090                        }
1091                        _ => None,
1092                    }
1093                }
1094                None => self.value(tag),
1095            };
1096            match value {
1097                Some(v) => {
1098                    result.value = Some(v);
1099                    result.data_type = Some("DINT".into());
1100                }
1101                None => {
1102                    let message = format!("Tag doesn't exist - {tag}");
1103                    result.error = Some(TagError::new(message, ErrorKind::NotFound));
1104                }
1105            }
1106            result
1107        }
1108    }
1109
1110    impl Plc for Fake {
1111        fn open(&mut self) -> Result<(), String> {
1112            Ok(())
1113        }
1114
1115        fn close(&mut self) {}
1116
1117        fn read(&mut self, tags: &[&str]) -> Result<Vec<TagResult>, String> {
1118            let names = tags.iter().map(|t| (*t).to_owned()).collect();
1119            self.log.lock().expect("log").push(Event::Read(names));
1120            let range = Regex::new(r"^(.+?)(?:\[(\d+)\])?\{(\d+)\}$").expect("regex");
1121            Ok(tags.iter().map(|tag| self.result(tag, &range)).collect())
1122        }
1123
1124        fn write(&mut self, _tag: &str, _value: &Value) -> Result<TagResult, String> {
1125            Err("not used".into())
1126        }
1127
1128        fn catalog(&mut self) -> Catalog {
1129            self.catalog.clone()
1130        }
1131
1132        fn version(&mut self) -> Json {
1133            Json::Null
1134        }
1135
1136        fn connection_size(&self) -> usize {
1137            PACKET_BYTES
1138        }
1139    }
1140
1141    // -------------------------------------------------------------------------------------------- helpers
1142
1143    type Grouped = BTreeMap<String, Vec<String>>;
1144    type Got = HashMap<String, (Option<Value>, Option<TagError>)>;
1145    type Together<'a> = Option<&'a dyn Fn(&str) -> bool>;
1146
1147    fn owned(tags: &[&str]) -> Vec<String> {
1148        tags.iter().map(|t| (*t).to_owned()).collect()
1149    }
1150
1151    fn set(names: &[&str]) -> HashSet<String> {
1152        names.iter().map(|t| (*t).to_owned()).collect()
1153    }
1154
1155    fn grouped(reads: Vec<Read>) -> Grouped {
1156        reads
1157            .into_iter()
1158            .map(|r| {
1159                let mut tags: Vec<String> = r.members.into_iter().map(|(t, _)| t).collect();
1160                tags.sort();
1161                (r.name, tags)
1162            })
1163            .collect()
1164    }
1165
1166    /// {read name: sorted requested tags it serves}
1167    fn reads_with(
1168        c: &Catalog,
1169        tags: &[&str],
1170        whole_max: u64,
1171        no_group: &[&str],
1172        budget: usize,
1173        together: Together,
1174    ) -> Grouped {
1175        grouped(plan(
1176            &owned(tags),
1177            c,
1178            whole_max,
1179            &set(no_group),
1180            budget,
1181            together,
1182        ))
1183    }
1184
1185    fn reads(c: &Catalog, tags: &[&str]) -> Grouped {
1186        reads_with(c, tags, 1500, &[], 4000, None)
1187    }
1188
1189    fn expect(pairs: &[(&str, &[&str])]) -> Grouped {
1190        pairs
1191            .iter()
1192            .map(|(name, tags)| ((*name).to_owned(), owned(tags)))
1193            .collect()
1194    }
1195
1196    fn plain(tags: &[&str]) -> Grouped {
1197        tags.iter()
1198            .map(|t| ((*t).to_owned(), vec![(*t).to_owned()]))
1199            .collect()
1200    }
1201
1202    fn got(results: HashMap<String, TagResult>) -> Got {
1203        results
1204            .into_iter()
1205            .map(|(t, r)| (t, (r.value, r.error)))
1206            .collect()
1207    }
1208
1209    /// What reading each tag on its own gives.
1210    fn direct(fake: &mut Fake, tags: &[&str]) -> Got {
1211        let results = fake.read(tags).expect("read");
1212        got(owned(tags).into_iter().zip(results).collect())
1213    }
1214
1215    /// Plans, chunks and executes `tags`: (results of every chunk, failed group reads, services).
1216    fn run(fake: &mut Fake, tags: &[&str], together: Together) -> (Got, Vec<String>, usize) {
1217        let c = fake.catalog.clone();
1218        let parts = chunks(
1219            plan(&owned(tags), &c, 1500, &set(&[]), 4000, together),
1220            &c,
1221            4000,
1222        );
1223        let mut given = HashMap::new();
1224        let (out, failed, services) = execute(fake, &parts, &mut |_, part| {
1225            given.extend(part);
1226            Ok(())
1227        })
1228        .expect("execute");
1229        given.extend(out);
1230        (got(given), failed, services)
1231    }
1232
1233    fn sizes(names: &[&str], c: &Catalog) -> Vec<(usize, usize)> {
1234        names.iter().map(|n| read_size(n, c)).collect()
1235    }
1236
1237    // -------------------------------------------------------------------------------------------- tests
1238
1239    #[test]
1240    fn tokenize_splits_names_and_refuses_what_is_never_grouped() {
1241        let toks = |name: &str| {
1242            tokenize(name).map(|t| t.into_iter().map(|t| (t.text, t.key)).collect::<Vec<_>>())
1243        };
1244        let n = |s: &str| Tok::Name(s.into());
1245        let expected = vec![
1246            ("Arr".into(), n("Arr")),
1247            ("[3]".into(), Tok::Index(3)),
1248            (".Status".into(), n("Status")),
1249            (".Ready".into(), n("Ready")),
1250        ];
1251        assert_eq!(toks("Arr[3].Status.Ready"), Some(expected));
1252        let expected = vec![
1253            ("Program:Station1.Motor".into(), n("Program:Station1.Motor")),
1254            (".Speed".into(), n("Speed")),
1255        ];
1256        assert_eq!(toks("Program:Station1.Motor.Speed"), Some(expected));
1257        assert_eq!(
1258            toks("M[1,2].X").expect("tokens")[1],
1259            ("[1,2]".into(), Tok::Multi(vec![1, 2]))
1260        );
1261        for never in ["X.Count.5", "X{3}", "X..Y", "[1]", "Program:Main", ""] {
1262            assert_eq!(toks(never), None, "{never}");
1263        }
1264    }
1265
1266    #[test]
1267    fn groups_members_and_merges_child_groups_into_the_parent() {
1268        let c = catalog();
1269        assert_eq!(
1270            reads(&c, &["Line1.Motor.Speed", "Line1.Motor.Current", "Speed"]),
1271            expect(&[
1272                ("Line1.Motor", &["Line1.Motor.Current", "Line1.Motor.Speed"]),
1273                ("Speed", &["Speed"])
1274            ])
1275        );
1276        // Line1.Motor (40 B) first, then Line1 (200 B) takes it and two more: 4 reads -> 1
1277        let tags = [
1278            "Line1.Motor.Speed",
1279            "Line1.Motor.Current",
1280            "Line1.Ready",
1281            "Line1.Data[2]",
1282            "Speed",
1283        ];
1284        let line1: &[&str] = &[
1285            "Line1.Data[2]",
1286            "Line1.Motor.Current",
1287            "Line1.Motor.Speed",
1288            "Line1.Ready",
1289        ];
1290        assert_eq!(
1291            reads(&c, &tags),
1292            expect(&[("Line1", line1), ("Speed", &["Speed"])])
1293        );
1294        let tags = ["Line1.Motor.Speed", "Line2.Motor.Speed"]; // no common struct
1295        assert_eq!(reads(&c, &tags), plain(&tags));
1296    }
1297
1298    #[test]
1299    fn array_elements_and_program_tags() {
1300        let tags = [
1301            "Arr[3].Motor.Speed",
1302            "Arr[3].Ready",
1303            "Arr[1].Ready",
1304            "Program:Station1.Motor.Speed",
1305            "Program:Station1.Motor.Running",
1306        ];
1307        let program: &[&str] = &[
1308            "Program:Station1.Motor.Running",
1309            "Program:Station1.Motor.Speed",
1310        ];
1311        assert_eq!(
1312            reads(&catalog(), &tags),
1313            expect(&[
1314                ("Arr[3]", &["Arr[3].Motor.Speed", "Arr[3].Ready"]),
1315                ("Program:Station1.Motor", program),
1316                ("Arr[1].Ready", &["Arr[1].Ready"]), // a whole array is never read for its elements
1317            ])
1318        );
1319    }
1320
1321    #[test]
1322    fn whole_max_bytes_caps_the_struct_size() {
1323        let c = blob(300);
1324        let tags = ["Blob1.A", "Blob1.B"];
1325        assert_eq!(reads(&c, &tags), expect(&[("Blob1", &tags)]));
1326        assert_eq!(reads_with(&c, &tags, 299, &[], 4000, None), plain(&tags));
1327        let tags = ["Line1.Motor.Speed", "Line1.Ready"];
1328        assert_eq!(reads_with(&c, &tags, 100, &[], 4000, None), plain(&tags));
1329    }
1330
1331    #[test]
1332    fn extra_reply_bytes_per_read_saved_threshold() {
1333        // reply: size + 10 whole vs 2 x (4 + 8) plain, so 450 extra bytes for one saved read at 464
1334        let tags = ["Blob1.A", "Blob1.B"];
1335        assert_eq!(reads(&blob(464), &tags), expect(&[("Blob1", &tags)]));
1336        assert_eq!(reads(&blob(465), &tags), plain(&tags));
1337        // a big struct is fine when it saves enough reads (Big1 is 4000 B, never with the default cap)
1338        assert_eq!(
1339            reads_with(&catalog(), &["Big1.A", "Big1.B"], 5000, &[], 4000, None).len(),
1340            2
1341        );
1342    }
1343
1344    #[test]
1345    fn a_group_never_adds_a_packet() {
1346        let c = catalog();
1347        let tags = ["Line1.Motor.Speed", "Line1.Motor.Current"];
1348        let plain_sizes: Vec<_> = tags
1349            .iter()
1350            .map(|t| (request_size(t, &c), reply_size(c.info(t))))
1351            .collect();
1352        let group = [(
1353            request_size("Line1.Motor", &c),
1354            reply_size(c.info("Line1.Motor")),
1355        )];
1356        let plain_reply: usize = plain_sizes.iter().map(|s| s.1).sum();
1357        let plain_request: usize = plain_sizes.iter().map(|s| s.0).sum();
1358        assert!(group[0].1 > plain_reply); // the whole read is the bigger reply
1359        let budget = (52 + plain_reply).max(30 + plain_request) + 10; // room for the plain reads,
1360        assert!(52 + group[0].1 > budget); // not for the whole reply
1361        assert_eq!(packets(&plain_sizes, budget), 1);
1362        assert_eq!(packets(&group, budget), 2 + 2); // fragmented: version checks around
1363        assert_eq!(reads_with(&c, &tags, 1500, &[], budget, None), plain(&tags));
1364        assert_eq!(
1365            reads(&c, &tags),
1366            expect(&[("Line1.Motor", &[tags[1], tags[0]])])
1367        );
1368    }
1369
1370    #[test]
1371    fn packet_estimate_packs_in_order() {
1372        assert_eq!(packets(&[(30, 1000); 3], 4000), 1);
1373        assert_eq!(packets(&[(30, 1000); 4], 4000), 2); // 52 bytes of reply overhead per packet
1374        assert_eq!(packets(&[(30, 5000)], 4000), 2 + 2); // fragmented, with a version check before and after
1375        assert_eq!(packets(&[], 4000), 0);
1376    }
1377
1378    #[test]
1379    fn aois_are_read_whole_only_when_the_probe_allows() {
1380        let c = catalog();
1381        assert_eq!(reads(&c, &["Valve1.Open", "Valve1.EnableIn"]).len(), 2); // whole read refused
1382        let tags = ["Pump1.Run", "Pump1.Speed"];
1383        assert_eq!(reads(&c, &tags), expect(&[("Pump1", &tags)]));
1384        assert_eq!(reads(&c, &["Cell1.Mode", "Cell1.Step"]).len(), 2); // holds such an AOI
1385        let tags = ["Cell1.Valve.Open", "Cell1.Mode"];
1386        assert_eq!(reads(&c, &tags), plain(&tags));
1387    }
1388
1389    #[test]
1390    fn never_grouped() {
1391        // Line1.Bits[3] is an element of a BOOL array: never sliced out of Line1
1392        let tags = [
1393            "Line1.Motor.Count.0",
1394            "Line1.Motor.Count.2",
1395            "Line1.Data{2}",
1396            "Line1.Motor.Speed",
1397            "Line1.Bits[3]",
1398            "Line1.Data",
1399            "Name1.LEN",
1400            "Name1.DATA",
1401            "Nope.A",
1402            "Nope.B",
1403        ];
1404        assert_eq!(reads(&catalog(), &tags), plain(&tags));
1405    }
1406
1407    #[test]
1408    fn requested_struct_is_the_group_read() {
1409        let c = catalog();
1410        let tags = ["Line1.Motor", "Line1.Motor.Speed", "Line1.Motor.Running"];
1411        let mut sorted = tags;
1412        sorted.sort_unstable();
1413        assert_eq!(reads(&c, &tags), expect(&[("Line1.Motor", &sorted)]));
1414        // also when it is too big to be chosen otherwise
1415        assert_eq!(
1416            reads(&c, &["Big1", "Big1.A"]),
1417            expect(&[("Big1", &["Big1", "Big1.A"])])
1418        );
1419        // and it joins a bigger group when there is one anyway
1420        let tags = ["Line1.Motor", "Line1.Motor.Speed", "Line1.Ready"];
1421        assert_eq!(reads(&c, &tags), expect(&[("Line1", &tags)]));
1422    }
1423
1424    #[test]
1425    fn sliced_values_are_what_member_reads_give() {
1426        let mut fake = Fake::new(catalog());
1427        let tags = [
1428            "Line1.Motor.Speed",
1429            "line1.motor.current",
1430            "Line1.Ready",
1431            "Line1.Data[2]",
1432            "Arr[3].Motor.Speed",
1433            "Arr[3].Data[0]",
1434            "Program:Station1.Motor.Speed",
1435            "Program:Station1.Motor.Count",
1436            "Line2.Motor",
1437            "Line2.Motor.Running",
1438            "Pump1.Speed",
1439            "Pump1.Run",
1440            "Speed",
1441            "Count.1",
1442            "Nope",
1443        ];
1444        let (got, failed, services) = run(&mut fake, &tags, None);
1445        assert_eq!(got, direct(&mut fake, &tags));
1446        assert!(failed.is_empty());
1447        // Line1, Arr[3], Program:Station1.Motor, Line2.Motor, Pump1, Speed, Count.1, Nope
1448        assert_eq!(services, 8);
1449    }
1450
1451    #[test]
1452    fn group_error_falls_back_to_member_reads() {
1453        let mut fake = Fake::new(catalog());
1454        fake.errors
1455            .insert("Line1.Motor".into(), "General error".into());
1456        let tags = ["Line1.Motor.Speed", "Line1.Motor.Current", "Speed"];
1457        let (got, failed, services) = run(&mut fake, &tags, None);
1458        assert_eq!(
1459            fake.reads()[1],
1460            owned(&["Line1.Motor.Speed", "Line1.Motor.Current"])
1461        );
1462        assert_eq!(got, direct(&mut fake, &tags));
1463        assert_eq!((failed, services), (owned(&["Line1.Motor"]), 4));
1464        // once known bad, it isn't read whole again; the next struct up still can be
1465        let c = catalog();
1466        assert_eq!(
1467            reads_with(&c, &tags, 1500, &["line1.motor"], 4000, None),
1468            expect(&[
1469                ("Line1", &["Line1.Motor.Current", "Line1.Motor.Speed"]),
1470                ("Speed", &["Speed"])
1471            ])
1472        );
1473        assert_eq!(
1474            reads_with(&c, &tags, 1500, &["line1.motor", "line1"], 4000, None),
1475            plain(&tags)
1476        );
1477    }
1478
1479    #[test]
1480    fn slice_failure_falls_back() {
1481        let mut fake = Fake::new(catalog());
1482        if let Some(Value::Struct(line1)) = fake.values.get_mut("Line1") {
1483            line1.insert("Motor".into(), Value::DInt(5)); // the whole read no longer holds the member
1484        }
1485        fake.values
1486            .insert("Line1.Motor.Speed".into(), Value::Real(1.0));
1487        fake.values
1488            .insert("Line1.Motor.Current".into(), Value::Real(2.0));
1489        let (got, failed, _) = run(
1490            &mut fake,
1491            &["Line1.Motor.Speed", "Line1.Ready", "Line1.Motor.Current"],
1492            None,
1493        );
1494        assert_eq!(got["Line1.Motor.Speed"], (Some(Value::Real(1.0)), None));
1495        assert_eq!(got["Line1.Motor.Current"], (Some(Value::Real(2.0)), None));
1496        assert_eq!(failed, owned(&["Line1"]));
1497    }
1498
1499    #[test]
1500    fn array_runs() {
1501        let bit_strings = vec![
1502            ("Words", atomic("WORD", &[10])),
1503            ("Bytes", atomic("BYTE", &[10])),
1504        ];
1505        let c = catalog_with(bit_strings, vec![]);
1506        assert_eq!(
1507            reads(&c, &["Trend[3]", "Trend[4]", "Trend[7]", "Speed"]),
1508            expect(&[
1509                ("Trend[3]{5}", &["Trend[3]", "Trend[4]", "Trend[7]"]),
1510                ("Speed", &["Speed"])
1511            ])
1512        );
1513        // a member array: the run is cheaper than the whole struct
1514        assert_eq!(
1515            reads(
1516                &c,
1517                &[
1518                    "Line1.Data[0]",
1519                    "Line1.Data[3]",
1520                    "Arr[2].Data[1]",
1521                    "Arr[2].Data[2]"
1522                ]
1523            ),
1524            expect(&[
1525                ("Line1.Data[0]{4}", &["Line1.Data[0]", "Line1.Data[3]"]),
1526                ("Arr[2].Data[1]{2}", &["Arr[2].Data[1]", "Arr[2].Data[2]"]),
1527            ])
1528        );
1529        // never runs: BOOL bits, bit string elements (new in the Rust port), struct elements, members of
1530        // elements, 2-D, whole arrays, a single element
1531        let never: [&[&str]; 9] = [
1532            &["BoolArr[1]", "BoolArr[2]"],
1533            &["Line1.Bits[0]", "Line1.Bits[1]"],
1534            &["Words[1]", "Words[2]"],
1535            &["Bytes[1]", "Bytes[2]"],
1536            &["Tank[1].Level", "Tank[2].Level"],
1537            &["Grid[1,1]", "Grid[1,2]"],
1538            &["Trend", "Trend[1]"],
1539            &["Trend[5]", "Speed"],
1540            &["Arr[1]", "Arr[2]"],
1541        ];
1542        for tags in never {
1543            assert_eq!(reads(&c, tags), plain(tags), "{tags:?}");
1544        }
1545        // a run that failed is not used again, nor is anything of a failed array
1546        let tags = ["Trend[3]", "Trend[4]"];
1547        assert_eq!(
1548            reads_with(&c, &tags, 1500, &["trend[3]{2}"], 4000, None).len(),
1549            2
1550        );
1551        assert_eq!(reads_with(&c, &tags, 1500, &["trend"], 4000, None).len(), 2);
1552    }
1553
1554    #[test]
1555    fn array_run_cost_rule() {
1556        let c = catalog();
1557        // REAL: (last + 1) * 4 + 8 reply bytes vs 2 x 12 plain: 440 extra bytes for one saved read at 113
1558        for (last, grouped) in [(113, true), (114, false)] {
1559            let last_tag = format!("Long[{last}]");
1560            let tags = ["Long[0]", last_tag.as_str()];
1561            let run = BTreeMap::from([(format!("Long[0]{{{}}}", last + 1), owned(&tags))]);
1562            assert_eq!(reads(&c, &tags) == run, grouped, "{last}");
1563        }
1564        // far apart elements make separate runs
1565        assert_eq!(
1566            reads(&c, &["Long[0]", "Long[1]", "Long[500]", "Long[502]"]),
1567            expect(&[
1568                ("Long[0]{2}", &["Long[0]", "Long[1]"]),
1569                ("Long[500]{3}", &["Long[500]", "Long[502]"])
1570            ])
1571        );
1572    }
1573
1574    #[test]
1575    fn array_run_never_adds_a_packet() {
1576        let c = catalog();
1577        let tags = ["Trend[0]", "Trend[15]"];
1578        let plain_sizes = sizes(&tags, &c);
1579        let whole = sizes(&["Trend[0]{16}"], &c);
1580        assert_eq!((whole[0].1, plain_sizes[0].1), (16 * 4 + 8, 12));
1581        assert_eq!(
1582            (packets(&plain_sizes, 100), packets(&whole, 100)),
1583            (1, 2 + 2)
1584        );
1585        assert_eq!(reads_with(&c, &tags, 1500, &[], 100, None), plain(&tags));
1586        assert_eq!(reads(&c, &tags), expect(&[("Trend[0]{16}", &tags)]));
1587    }
1588
1589    #[test]
1590    fn array_run_values_are_sliced() {
1591        let mut fake = Fake::new(catalog());
1592        let tags = [
1593            "Trend[3]",
1594            "Trend[4]",
1595            "Trend[7]",
1596            "Line1.Data[0]",
1597            "Line1.Data[3]",
1598            "Line1.Ready",
1599            "Speed",
1600        ];
1601        let (got, failed, services) = run(&mut fake, &tags, None);
1602        assert_eq!(got, direct(&mut fake, &tags));
1603        assert!(failed.is_empty());
1604        assert_eq!(got["Trend[7]"], (Some(Value::Real(3.5)), None));
1605        assert_eq!(services, 3); // Trend run, Line1 (it took the Data run), Speed
1606        // the run now reads past the end: its members are read one by one
1607        fake.values.insert(
1608            "Trend".into(),
1609            Value::Array(vec![Value::Real(1.0), Value::Real(2.0)]),
1610        );
1611        let (got, failed, _) = run(&mut fake, &["Trend[0]", "Trend[1]", "Trend[3]"], None);
1612        assert_eq!(failed, owned(&["Trend[0]{4}"]));
1613        assert_eq!(got["Trend[1]"], (Some(Value::Real(2.0)), None));
1614        assert_eq!(
1615            got["Trend[3]"].1.as_ref().map(|e| e.kind),
1616            Some(ErrorKind::NotFound)
1617        );
1618    }
1619
1620    #[test]
1621    fn together_reads_a_struct_whole() {
1622        let c = catalog();
1623        let all = |_: &str| true;
1624        let line1 = |p: &str| p == "Line1";
1625        let tags = ["Line1.Motor.Speed", "Line1.Ready"];
1626        assert_eq!(reads_with(&c, &tags, 100, &[], 4000, None), plain(&tags));
1627        assert_eq!(
1628            reads_with(&c, &tags, 100, &[], 4000, Some(&line1)),
1629            expect(&[("Line1", &tags)])
1630        );
1631        // regardless of cost; outermost first; never for one tag or a struct over one packet
1632        let tags = ["Blob1.A", "Blob1.B"];
1633        assert_eq!(
1634            reads_with(&blob(3000), &tags, 1500, &[], 4000, Some(&all)),
1635            expect(&[("Blob1", &tags)])
1636        );
1637        let tags = ["Line1.Motor.Speed", "Line1.Motor.Current", "Line1.Ready"];
1638        assert_eq!(
1639            reads_with(&c, &tags, 1500, &[], 4000, Some(&all)),
1640            expect(&[(
1641                "Line1",
1642                &["Line1.Motor.Current", "Line1.Motor.Speed", "Line1.Ready"]
1643            )])
1644        );
1645        let two = ["Line1.Motor.Speed", "Speed"];
1646        assert_eq!(
1647            reads_with(&c, &two, 1500, &[], 4000, Some(&all)),
1648            plain(&two)
1649        );
1650        let big = ["Big1.A", "Big1.B"];
1651        assert_eq!(
1652            reads_with(&c, &big, 1500, &[], 4000, Some(&all)),
1653            plain(&big)
1654        );
1655        let seen = RefCell::new(Vec::new());
1656        let record = |p: &str| {
1657            seen.borrow_mut().push(p.to_owned());
1658            false
1659        };
1660        reads_with(
1661            &c,
1662            &["Arr[1].Motor.Speed", "Arr[1].Motor.Count"],
1663            1500,
1664            &[],
1665            4000,
1666            Some(&record),
1667        );
1668        assert_eq!(seen.into_inner(), owned(&["Arr[1]", "Arr[1].Motor"]));
1669        // a failed together read falls back to its members like any group
1670        let mut fake = Fake::new(catalog());
1671        fake.errors.insert("Line1".into(), "General error".into());
1672        let (got, failed, _) = run(&mut fake, &tags, Some(&line1));
1673        assert_eq!(failed, owned(&["Line1"]));
1674        assert_eq!(got, direct(&mut fake, &tags));
1675    }
1676
1677    #[test]
1678    fn chunks_are_one_packet_each() {
1679        let c = catalog();
1680        let mut tags = owned(&["Line1.Motor.Speed", "Line2.Motor.Speed"]);
1681        tags.extend((0..16).step_by(5).map(|i| format!("Trend[{i}]")));
1682        tags.extend((0..5).map(|i| format!("Arr[{i}].Ready")));
1683        tags.extend(owned(&["Speed", "Count", "Name1", "Big1", "Flags"]));
1684        let budget = 200;
1685        let planned = plan(&tags, &c, 1500, &set(&[]), budget, None);
1686        let parts = chunks(planned.clone(), &c, budget);
1687        let mut flat: Vec<Read> = parts.iter().flatten().cloned().collect();
1688        let mut sorted = planned;
1689        flat.sort();
1690        sorted.sort();
1691        assert_eq!(flat, sorted);
1692        assert_eq!(parts.len(), 3);
1693        // as ciplink: the fragmented read after the others
1694        let big1 = Read {
1695            name: "Big1".into(),
1696            members: vec![("Big1".into(), None)],
1697        };
1698        assert_eq!(parts[2], vec![big1]);
1699        let part_sizes: Vec<Vec<(usize, usize)>> = parts
1700            .iter()
1701            .map(|part| part.iter().map(|r| read_size(&r.name, &c)).collect())
1702            .collect();
1703        let big = packets(&sizes(&["Big1"], &c), budget); // fragmented: alone, several packets
1704        let all: Vec<(usize, usize)> = part_sizes.iter().flatten().copied().collect();
1705        assert!(big > 1);
1706        assert_eq!(parts.len(), packets(&all, budget) - big + 1);
1707        for (part, size) in parts.iter().zip(&part_sizes) {
1708            assert!(
1709                packets(size, budget) == 1 || part.iter().map(|r| r.name.as_str()).eq(["Big1"])
1710            );
1711        }
1712        assert!(chunks(Vec::new(), &c, PACKET_BYTES).is_empty());
1713    }
1714
1715    #[test]
1716    fn a_fragmented_read_does_not_split_the_reads_around_it() {
1717        let sizes = [(30, 100), (30, 5000), (30, 100)];
1718        assert_eq!(packets(&sizes, 4000), 5); // one packet for both small reads
1719        assert_eq!(pack(&sizes, 4000).0, vec![vec![0, 2], vec![1]]);
1720    }
1721
1722    #[test]
1723    fn read_size_of_ranges() {
1724        let c = catalog_with(vec![("Bits", atomic("DWORD", &[128]))], vec![]); // ciplink's BOOL[128]
1725        let station = reply_size(c.info("Arr")); // structure_size + 10
1726        assert_eq!(read_size("Arr{4}", &c).1, 4 * (station - 10) + 10);
1727        assert_eq!(read_size("Bits{64}", &c).1, 2 * 4 + 8);
1728        assert_eq!(read_size("Trend[2]{3}", &c).1, 3 * 4 + 8);
1729    }
1730
1731    #[test]
1732    fn between_runs_after_each_chunk_but_the_last() {
1733        let mut fake = Fake::new(catalog());
1734        let c = fake.catalog.clone();
1735        let tags = [
1736            "Line1.Motor.Speed",
1737            "Line1.Motor.Current",
1738            "Speed",
1739            "Count",
1740            "Trend[1]",
1741            "Trend[2]",
1742            "Flags",
1743            "Name1",
1744        ];
1745        let planned = plan(&owned(&tags), &c, 1500, &set(&[]), 100, None);
1746        let count = planned.len();
1747        let parts = chunks(planned, &c, 100);
1748        assert!(parts.len() >= 3);
1749        let log = Arc::clone(&fake.log);
1750        let mut given = HashMap::new();
1751        let (out, failed, services) = execute(&mut fake, &parts, &mut |_, part| {
1752            let mut names: Vec<String> = part.keys().cloned().collect();
1753            names.sort();
1754            log.lock().expect("log").push(Event::Between(names));
1755            given.extend(part);
1756            Ok(())
1757        })
1758        .expect("execute");
1759        let mut expected = Vec::new();
1760        for (i, part) in parts.iter().enumerate() {
1761            expected.push(Event::Read(part.iter().map(|r| r.name.clone()).collect()));
1762            if i + 1 < parts.len() {
1763                let mut names: Vec<String> = part
1764                    .iter()
1765                    .flat_map(|r| r.members.iter().map(|(t, _)| t.clone()))
1766                    .collect();
1767                names.sort();
1768                expected.push(Event::Between(names));
1769            }
1770        }
1771        assert_eq!(*fake.log.lock().expect("log"), expected);
1772        assert!(out.keys().all(|t| !given.contains_key(t)));
1773        given.extend(out);
1774        assert_eq!(given.keys().cloned().collect::<HashSet<_>>(), set(&tags));
1775        assert_eq!(got(given), direct(&mut fake, &tags));
1776        assert_eq!((services, failed.len()), (count, 0));
1777    }
1778
1779    #[test]
1780    fn failed_group_is_read_after_the_last_chunk() {
1781        let mut fake = Fake::new(catalog());
1782        let c = fake.catalog.clone();
1783        fake.errors
1784            .insert("Line1.Motor".into(), "General error".into());
1785        let tags = [
1786            "Line1.Motor.Speed",
1787            "Line1.Motor.Current",
1788            "Speed",
1789            "Count",
1790            "Flags",
1791            "Name1",
1792        ];
1793        let parts = chunks(plan(&owned(&tags), &c, 1500, &set(&[]), 150, None), &c, 150);
1794        assert!(parts.len() >= 2 && parts[0][0].name == "Line1.Motor");
1795        let mut given = HashMap::new();
1796        let (out, failed, _) = execute(&mut fake, &parts, &mut |_, part| {
1797            given.extend(part);
1798            Ok(())
1799        })
1800        .expect("execute");
1801        let sent = fake.reads();
1802        assert_eq!(sent.len(), parts.len() + 1);
1803        assert_eq!(
1804            sent.last(),
1805            Some(&owned(&["Line1.Motor.Speed", "Line1.Motor.Current"]))
1806        );
1807        assert_eq!(failed, owned(&["Line1.Motor"]));
1808        assert!(out.contains_key("Line1.Motor.Speed") && !given.contains_key("Line1.Motor.Speed"));
1809        given.extend(out);
1810        assert_eq!(got(given), direct(&mut fake, &tags));
1811    }
1812
1813    #[test]
1814    fn execute_takes_a_plain_plan_or_nothing() {
1815        let mut fake = Fake::new(catalog());
1816        let c = fake.catalog.clone();
1817        let planned = plan(
1818            &owned(&["Speed", "Count"]),
1819            &c,
1820            1500,
1821            &set(&[]),
1822            PACKET_BYTES,
1823            None,
1824        );
1825        let mut never =
1826            |_: &mut dyn Plc, _| -> Result<(), String> { panic!("one chunk: no between") };
1827        let (out, failed, services) = execute(&mut fake, &[planned], &mut never).expect("execute");
1828        let values: HashMap<String, Option<Value>> =
1829            out.into_iter().map(|(t, r)| (t, r.value)).collect();
1830        let expected = HashMap::from([
1831            ("Speed".to_owned(), Some(Value::Real(3.5))),
1832            ("Count".to_owned(), Some(Value::DInt(9))),
1833        ]);
1834        assert_eq!(values, expected);
1835        assert_eq!((failed.len(), services), (0, 2));
1836        let (out, failed, services) = execute(&mut fake, &[], &mut never).expect("execute");
1837        assert!(out.is_empty() && failed.is_empty() && services == 0);
1838    }
1839
1840    #[test]
1841    fn execute_passes_on_a_lost_connection() {
1842        let mut fake = Fake::new(catalog());
1843        let parts = vec![vec![Read {
1844            name: "Speed".into(),
1845            members: vec![("Speed".into(), None)],
1846        }]];
1847        struct Lost<'a>(&'a mut Fake);
1848        impl Plc for Lost<'_> {
1849            fn open(&mut self) -> Result<(), String> {
1850                Ok(())
1851            }
1852            fn close(&mut self) {}
1853            fn read(&mut self, _: &[&str]) -> Result<Vec<TagResult>, String> {
1854                Err("connection lost".into())
1855            }
1856            fn write(&mut self, tag: &str, value: &Value) -> Result<TagResult, String> {
1857                self.0.write(tag, value)
1858            }
1859            fn catalog(&mut self) -> Catalog {
1860                self.0.catalog()
1861            }
1862            fn version(&mut self) -> Json {
1863                Json::Null
1864            }
1865            fn connection_size(&self) -> usize {
1866                PACKET_BYTES
1867            }
1868        }
1869        let result = execute(&mut Lost(&mut fake), &parts, &mut |_, _| Ok(()));
1870        assert_eq!(result, Err("connection lost".into()));
1871    }
1872
1873    #[test]
1874    fn request_size_follows_the_instance_id_and_assumes_16_bits() {
1875        let tag = |id| Node {
1876            instance_id: Some(id),
1877            ..atomic("DINT", &[])
1878        };
1879        let tags = [("small", tag(5)), ("mid", tag(300)), ("big", tag(70000))];
1880        let tags = tags.into_iter().map(|(k, v)| (k.to_owned(), v)).collect();
1881        let c = Catalog::new(Json::Null, tags, IndexMap::new());
1882        let got: Vec<usize> = ["small", "mid", "big"]
1883            .iter()
1884            .map(|n| request_size(&format!("{n}.Level"), &c))
1885            .collect();
1886        assert_eq!(got, vec![18, 20, 22]);
1887        assert_eq!(request_size("Motor7.Level", &c), 20); // unknown: 16 bits
1888    }
1889
1890    #[test]
1891    fn a_run_that_turns_fragmented_is_not_taken_for_free() {
1892        let tags = IndexMap::from([("Trend".to_owned(), atomic("REAL", &[2000]))]);
1893        let c = Catalog::new(Json::Null, tags, IndexMap::new());
1894        let tags: Vec<String> = (0..1000)
1895            .step_by(2)
1896            .map(|i| format!("Trend[{i}]"))
1897            .collect();
1898        // ciplink sends it as check, 2 fragments, check: more than the 2 packets of plain reads
1899        assert_eq!(packets(&sizes(&["Trend[0]{999}"], &c), 4000), 2 + 2);
1900        assert_eq!(
1901            plan(&tags, &c, 1500, &set(&[]), 4000, None).len(),
1902            tags.len()
1903        );
1904    }
1905
1906    #[test]
1907    fn dig_slices_members_ignoring_case_and_elements() {
1908        let value = station(1);
1909        let path = [Key::Member("motor".into()), Key::Member("SPEED".into())];
1910        assert_eq!(dig(&value, &path), Some(Value::Real(1.5)));
1911        assert_eq!(
1912            dig(&value, &[Key::Member("Data".into()), Key::Index(3)]),
1913            Some(Value::DInt(4))
1914        );
1915        assert_eq!(
1916            dig(&value, &[Key::Member("Data".into()), Key::Index(4)]),
1917            None
1918        );
1919        assert_eq!(dig(&value, &[Key::Index(0)]), None); // not an array
1920        assert_eq!(dig(&Value::DInt(5), &[Key::Member("Speed".into())]), None); // not a struct
1921        assert_eq!(dig(&value, &[]), Some(value.clone()));
1922    }
1923}