Skip to main content

viva_genapi/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! GenApi node system: typed feature access backed by register IO.
3
4mod bitops;
5mod conversions;
6mod error;
7mod io;
8mod nodemap;
9mod nodes;
10mod swissknife;
11
12pub use error::GenApiError;
13pub use io::{NullIo, RegisterIo};
14pub use nodemap::NodeMap;
15pub use nodes::{
16    BooleanNode, CategoryNode, CommandNode, EnumNode, FloatNode, IntegerNode, Node, NodeMeta,
17    Representation, SkNode, Visibility,
18};
19pub use viva_genapi_xml::SkOutput;
20
21#[cfg(test)]
22mod tests {
23    use std::cell::RefCell;
24    use std::collections::HashMap;
25
26    use crate::conversions::{bytes_to_i64, i64_to_bytes};
27    use crate::{GenApiError, NodeMap, RegisterIo, Visibility};
28
29    const FIXTURE: &str = r#"
30        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
31            <Integer Name="Width">
32                <Address>0x100</Address>
33                <Length>4</Length>
34                <AccessMode>RW</AccessMode>
35                <Min>16</Min>
36                <Max>4096</Max>
37                <Inc>2</Inc>
38            </Integer>
39            <Float Name="ExposureTime">
40                <Address>0x200</Address>
41                <Length>4</Length>
42                <AccessMode>RW</AccessMode>
43                <Min>10.0</Min>
44                <Max>100000.0</Max>
45                <Scale>1/1000</Scale>
46            </Float>
47            <Enumeration Name="GainSelector">
48                <Address>0x300</Address>
49                <Length>2</Length>
50                <AccessMode>RW</AccessMode>
51                <EnumEntry Name="All" Value="0" />
52                <EnumEntry Name="Red" Value="1" />
53                <EnumEntry Name="Blue" Value="2" />
54            </Enumeration>
55            <Integer Name="Gain">
56                <Length>2</Length>
57                <AccessMode>RW</AccessMode>
58                <Min>0</Min>
59                <Max>48</Max>
60                <pSelected>GainSelector</pSelected>
61                <Selected>All</Selected>
62                <Address>0x310</Address>
63                <Selected>Red</Selected>
64                <Address>0x314</Address>
65                <Selected>Blue</Selected>
66            </Integer>
67            <Boolean Name="GammaEnable">
68                <Address>0x400</Address>
69                <Length>1</Length>
70                <AccessMode>RW</AccessMode>
71            </Boolean>
72            <Command Name="AcquisitionStart">
73                <Address>0x500</Address>
74                <Length>4</Length>
75            </Command>
76        </RegisterDescription>
77    "#;
78
79    const INDIRECT_FIXTURE: &str = r#"
80        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
81            <Integer Name="RegAddr">
82                <Address>0x2000</Address>
83                <Length>4</Length>
84                <AccessMode>RW</AccessMode>
85                <Min>0</Min>
86                <Max>65535</Max>
87            </Integer>
88            <Integer Name="Gain">
89                <pAddress>RegAddr</pAddress>
90                <Length>4</Length>
91                <AccessMode>RW</AccessMode>
92                <Min>0</Min>
93                <Max>255</Max>
94            </Integer>
95        </RegisterDescription>
96    "#;
97
98    const ENUM_PVALUE_FIXTURE: &str = r#"
99        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
100            <Enumeration Name="Mode">
101                <Address>0x4000</Address>
102                <Length>4</Length>
103                <AccessMode>RW</AccessMode>
104                <EnumEntry Name="Fixed10">
105                    <Value>10</Value>
106                </EnumEntry>
107                <EnumEntry Name="DynFromReg">
108                    <pValue>RegModeVal</pValue>
109                </EnumEntry>
110            </Enumeration>
111            <Integer Name="RegModeVal">
112                <Address>0x4100</Address>
113                <Length>4</Length>
114                <AccessMode>RW</AccessMode>
115                <Min>0</Min>
116                <Max>65535</Max>
117            </Integer>
118        </RegisterDescription>
119    "#;
120
121    const BITFIELD_FIXTURE: &str = r#"
122        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
123            <Integer Name="LeByte">
124                <Address>0x5000</Address>
125                <Length>4</Length>
126                <AccessMode>RW</AccessMode>
127                <Min>0</Min>
128                <Max>65535</Max>
129                <Mask>0x0000FF00</Mask>
130            </Integer>
131            <Integer Name="BeBits">
132                <Address>0x5004</Address>
133                <Length>2</Length>
134                <AccessMode>RW</AccessMode>
135                <Min>0</Min>
136                <Max>15</Max>
137                <Lsb>13</Lsb>
138                <Msb>15</Msb>
139                <Endianness>BigEndian</Endianness>
140            </Integer>
141            <Boolean Name="PackedFlag">
142                <Address>0x5006</Address>
143                <Length>4</Length>
144                <AccessMode>RW</AccessMode>
145                <Bit>13</Bit>
146            </Boolean>
147        </RegisterDescription>
148    "#;
149
150    const SWISSKNIFE_FIXTURE: &str = r#"
151        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
152            <Integer Name="GainRaw">
153                <Address>0x3000</Address>
154                <Length>4</Length>
155                <AccessMode>RW</AccessMode>
156                <Min>0</Min>
157                <Max>1000</Max>
158            </Integer>
159            <Float Name="Offset">
160                <Address>0x3008</Address>
161                <Length>4</Length>
162                <AccessMode>RW</AccessMode>
163                <Min>-100.0</Min>
164                <Max>100.0</Max>
165            </Float>
166            <Integer Name="B">
167                <Address>0x3010</Address>
168                <Length>4</Length>
169                <AccessMode>RW</AccessMode>
170                <Min>-1000</Min>
171                <Max>1000</Max>
172            </Integer>
173            <SwissKnife Name="ComputedGain">
174                <Expression>(GainRaw * 0.5) + Offset</Expression>
175                <pVariable Name="GainRaw">GainRaw</pVariable>
176                <pVariable Name="Offset">Offset</pVariable>
177                <Output>Float</Output>
178            </SwissKnife>
179            <SwissKnife Name="DivideInt">
180                <Expression>GainRaw / 3</Expression>
181                <pVariable Name="GainRaw">GainRaw</pVariable>
182                <Output>Integer</Output>
183            </SwissKnife>
184            <SwissKnife Name="Unary">
185                <Expression>-GainRaw + 10</Expression>
186                <pVariable Name="GainRaw">GainRaw</pVariable>
187                <Output>Integer</Output>
188            </SwissKnife>
189            <SwissKnife Name="DivideByZero">
190                <Expression>GainRaw / B</Expression>
191                <pVariable Name="GainRaw">GainRaw</pVariable>
192                <pVariable Name="B">B</pVariable>
193                <Output>Float</Output>
194            </SwissKnife>
195        </RegisterDescription>
196    "#;
197
198    #[derive(Default)]
199    struct MockIo {
200        regs: RefCell<HashMap<u64, Vec<u8>>>,
201        reads: RefCell<HashMap<u64, usize>>,
202    }
203
204    impl MockIo {
205        fn with_registers(entries: &[(u64, Vec<u8>)]) -> Self {
206            let mut regs = HashMap::new();
207            for (addr, data) in entries {
208                regs.insert(*addr, data.clone());
209            }
210            MockIo {
211                regs: RefCell::new(regs),
212                reads: RefCell::new(HashMap::new()),
213            }
214        }
215
216        fn read_count(&self, addr: u64) -> usize {
217            *self.reads.borrow().get(&addr).unwrap_or(&0)
218        }
219    }
220
221    impl RegisterIo for MockIo {
222        fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
223            let mut reads = self.reads.borrow_mut();
224            *reads.entry(addr).or_default() += 1;
225            let regs = self.regs.borrow();
226            let data = regs
227                .get(&addr)
228                .ok_or_else(|| GenApiError::Io(format!("read miss at 0x{addr:08X}")))?;
229            if data.len() != len {
230                return Err(GenApiError::Io(format!(
231                    "length mismatch at 0x{addr:08X}: expected {len}, have {}",
232                    data.len()
233                )));
234            }
235            Ok(data.clone())
236        }
237
238        fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
239            self.regs.borrow_mut().insert(addr, data.to_vec());
240            Ok(())
241        }
242    }
243
244    fn build_nodemap() -> NodeMap {
245        let model = viva_genapi_xml::parse(FIXTURE).expect("parse fixture");
246        NodeMap::from(model)
247    }
248
249    fn build_indirect_nodemap() -> NodeMap {
250        let model = viva_genapi_xml::parse(INDIRECT_FIXTURE).expect("parse indirect fixture");
251        NodeMap::from(model)
252    }
253
254    fn build_enum_pvalue_nodemap() -> NodeMap {
255        let model = viva_genapi_xml::parse(ENUM_PVALUE_FIXTURE).expect("parse enum pvalue fixture");
256        NodeMap::from(model)
257    }
258
259    fn build_bitfield_nodemap() -> NodeMap {
260        let model = viva_genapi_xml::parse(BITFIELD_FIXTURE).expect("parse bitfield fixture");
261        NodeMap::from(model)
262    }
263
264    fn build_swissknife_nodemap() -> NodeMap {
265        let model = viva_genapi_xml::parse(SWISSKNIFE_FIXTURE).expect("parse swissknife fixture");
266        NodeMap::from(model)
267    }
268
269    #[test]
270    fn integer_roundtrip_and_cache() {
271        let mut nodemap = build_nodemap();
272        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 4, 0])]);
273        let width = nodemap.get_integer("Width", &io).expect("read width");
274        assert_eq!(width, 1024);
275        assert_eq!(io.read_count(0x100), 1);
276        let width_again = nodemap.get_integer("Width", &io).expect("cached width");
277        assert_eq!(width_again, 1024);
278        assert_eq!(io.read_count(0x100), 1, "cached value should be reused");
279        nodemap
280            .set_integer("Width", 1030, &io)
281            .expect("write width");
282        let width = nodemap
283            .get_integer("Width", &io)
284            .expect("read updated width");
285        assert_eq!(width, 1030);
286        assert_eq!(io.read_count(0x100), 1, "write should update cache");
287    }
288
289    #[test]
290    fn float_conversion_roundtrip() {
291        let mut nodemap = build_nodemap();
292        let raw = 50_000i64; // 50 ms with 1/1000 scale
293        let io = MockIo::with_registers(&[(0x200, i64_to_bytes("ExposureTime", raw, 4).unwrap())]);
294        let exposure = nodemap
295            .get_float("ExposureTime", &io)
296            .expect("read exposure");
297        assert!((exposure - 50.0).abs() < 1e-6);
298        nodemap
299            .set_float("ExposureTime", 75.0, &io)
300            .expect("write exposure");
301        let raw_back = bytes_to_i64("ExposureTime", &io.read(0x200, 4).unwrap()).unwrap();
302        assert_eq!(raw_back, 75_000);
303    }
304
305    #[test]
306    fn selector_address_switching() {
307        let mut nodemap = build_nodemap();
308        let io = MockIo::with_registers(&[
309            (0x300, i64_to_bytes("GainSelector", 0, 2).unwrap()),
310            (0x310, i64_to_bytes("Gain", 10, 2).unwrap()),
311            (0x314, i64_to_bytes("Gain", 24, 2).unwrap()),
312        ]);
313
314        let gain_all = nodemap.get_integer("Gain", &io).expect("gain for All");
315        assert_eq!(gain_all, 10);
316        assert_eq!(io.read_count(0x310), 1);
317        assert_eq!(io.read_count(0x314), 0);
318
319        io.write(0x314, &i64_to_bytes("Gain", 32, 2).unwrap())
320            .expect("update red gain");
321        nodemap
322            .set_enum("GainSelector", "Red", &io)
323            .expect("set selector to red");
324        let gain_red = nodemap.get_integer("Gain", &io).expect("gain for Red");
325        assert_eq!(gain_red, 32);
326        assert_eq!(
327            io.read_count(0x310),
328            1,
329            "previous address should not be reread"
330        );
331        assert_eq!(io.read_count(0x314), 1);
332
333        let gain_red_cached = nodemap.get_integer("Gain", &io).expect("cached red");
334        assert_eq!(gain_red_cached, 32);
335        assert_eq!(io.read_count(0x314), 1, "selector cache should be reused");
336
337        nodemap
338            .set_enum("GainSelector", "Blue", &io)
339            .expect("set selector to blue");
340        let err = nodemap.get_integer("Gain", &io).unwrap_err();
341        match err {
342            GenApiError::Unavailable(msg) => {
343                assert!(msg.contains("GainSelector=Blue"));
344            }
345            other => panic!("unexpected error: {other:?}"),
346        }
347        assert_eq!(
348            io.read_count(0x314),
349            1,
350            "no read expected for missing mapping"
351        );
352
353        io.write(0x310, &i64_to_bytes("Gain", 12, 2).unwrap())
354            .expect("update all gain");
355        nodemap
356            .set_enum("GainSelector", "All", &io)
357            .expect("restore selector to all");
358        let gain_all_updated = nodemap
359            .get_integer("Gain", &io)
360            .expect("gain for All again");
361        assert_eq!(gain_all_updated, 12);
362        assert_eq!(
363            io.read_count(0x310),
364            2,
365            "address switch should invalidate cache"
366        );
367    }
368
369    #[test]
370    fn range_enforcement() {
371        let mut nodemap = build_nodemap();
372        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 16])]);
373        let err = nodemap.set_integer("Width", 17, &io).unwrap_err();
374        assert!(matches!(err, GenApiError::Range(_)));
375    }
376
377    #[test]
378    fn command_exec() {
379        let mut nodemap = build_nodemap();
380        let io = MockIo::with_registers(&[]);
381        nodemap
382            .exec_command("AcquisitionStart", &io)
383            .expect("exec command");
384        let payload = io.read(0x500, 4).expect("command write");
385        assert_eq!(payload, vec![0, 0, 0, 1]);
386    }
387
388    #[test]
389    fn indirect_address_resolution() {
390        let mut nodemap = build_indirect_nodemap();
391        let io = MockIo::with_registers(&[
392            (0x2000, i64_to_bytes("RegAddr", 0x3000, 4).unwrap()),
393            (0x3000, i64_to_bytes("Gain", 123, 4).unwrap()),
394            (0x3100, i64_to_bytes("Gain", 77, 4).unwrap()),
395        ]);
396
397        let initial = nodemap.get_integer("Gain", &io).expect("read gain");
398        assert_eq!(initial, 123);
399        assert_eq!(io.read_count(0x2000), 1);
400        assert_eq!(io.read_count(0x3000), 1);
401
402        nodemap
403            .set_integer("RegAddr", 0x3100, &io)
404            .expect("set indirect address");
405        let updated = nodemap
406            .get_integer("Gain", &io)
407            .expect("read gain after change");
408        assert_eq!(updated, 77);
409        assert_eq!(io.read_count(0x2000), 1);
410        assert_eq!(io.read_count(0x3000), 1);
411        assert_eq!(io.read_count(0x3100), 1);
412    }
413
414    #[test]
415    fn indirect_bad_address() {
416        let mut nodemap = build_indirect_nodemap();
417        let io = MockIo::with_registers(&[(0x2000, vec![0, 0, 0, 0])]);
418
419        nodemap
420            .set_integer("RegAddr", 0, &io)
421            .expect("write zero address");
422        let err = nodemap.get_integer("Gain", &io).unwrap_err();
423        match err {
424            GenApiError::BadIndirectAddress { name, addr } => {
425                assert_eq!(name, "Gain");
426                assert_eq!(addr, 0);
427            }
428            other => panic!("unexpected error: {other:?}"),
429        }
430        assert_eq!(io.read_count(0x2000), 0);
431    }
432
433    #[test]
434    fn enum_literal_entry_read() {
435        let nodemap = build_enum_pvalue_nodemap();
436        let io = MockIo::with_registers(&[
437            (0x4000, i64_to_bytes("Mode", 10, 4).unwrap()),
438            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
439        ]);
440
441        let value = nodemap.get_enum("Mode", &io).expect("read mode");
442        assert_eq!(value, "Fixed10");
443        assert_eq!(
444            io.read_count(0x4100),
445            1,
446            "provider should be read once for mapping"
447        );
448    }
449
450    #[test]
451    fn enum_provider_entry_read() {
452        let nodemap = build_enum_pvalue_nodemap();
453        let io = MockIo::with_registers(&[
454            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
455            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
456        ]);
457
458        let value = nodemap.get_enum("Mode", &io).expect("read dynamic mode");
459        assert_eq!(value, "DynFromReg");
460        assert_eq!(io.read_count(0x4100), 1);
461    }
462
463    #[test]
464    fn enum_set_uses_provider_value() {
465        let mut nodemap = build_enum_pvalue_nodemap();
466        let io = MockIo::with_registers(&[
467            (0x4000, i64_to_bytes("Mode", 0, 4).unwrap()),
468            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
469        ]);
470
471        nodemap
472            .set_enum("Mode", "DynFromReg", &io)
473            .expect("write enum");
474        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
475        assert_eq!(raw, 42);
476        assert_eq!(io.read_count(0x4100), 1);
477    }
478
479    #[test]
480    fn enum_provider_update_invalidates_mapping() {
481        let mut nodemap = build_enum_pvalue_nodemap();
482        let io = MockIo::with_registers(&[
483            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
484            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
485        ]);
486
487        assert_eq!(nodemap.get_enum("Mode", &io).unwrap(), "DynFromReg");
488        assert_eq!(io.read_count(0x4100), 1);
489
490        nodemap
491            .set_integer("RegModeVal", 17, &io)
492            .expect("update provider");
493        io.write(0x4000, &i64_to_bytes("Mode", 0, 4).unwrap())
494            .expect("reset mode register");
495
496        nodemap
497            .set_enum("Mode", "DynFromReg", &io)
498            .expect("write enum after provider change");
499        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
500        assert_eq!(raw, 17);
501    }
502
503    #[test]
504    fn enum_unknown_value_error() {
505        let nodemap = build_enum_pvalue_nodemap();
506        let io = MockIo::with_registers(&[
507            (0x4000, i64_to_bytes("Mode", 99, 4).unwrap()),
508            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
509        ]);
510
511        let err = nodemap.get_enum("Mode", &io).unwrap_err();
512        match err {
513            GenApiError::EnumValueUnknown { node, value } => {
514                assert_eq!(node, "Mode");
515                assert_eq!(value, 99);
516            }
517            other => panic!("unexpected error: {other:?}"),
518        }
519    }
520
521    #[test]
522    fn enum_entries_are_sorted() {
523        let nodemap = build_enum_pvalue_nodemap();
524        let entries = nodemap.enum_entries("Mode").expect("entries");
525        assert_eq!(
526            entries,
527            vec!["DynFromReg".to_string(), "Fixed10".to_string()]
528        );
529    }
530
531    #[test]
532    fn bitfield_le_integer_roundtrip() {
533        let mut nodemap = build_bitfield_nodemap();
534        let io = MockIo::with_registers(&[(0x5000, vec![0xAA, 0xBB, 0xCC, 0xDD])]);
535
536        let value = nodemap
537            .get_integer("LeByte", &io)
538            .expect("read little-endian field");
539        assert_eq!(value, 0xBB);
540
541        nodemap
542            .set_integer("LeByte", 0x55, &io)
543            .expect("write little-endian field");
544        let data = io.read(0x5000, 4).expect("read back register");
545        assert_eq!(data, vec![0xAA, 0x55, 0xCC, 0xDD]);
546    }
547
548    #[test]
549    fn bitfield_be_integer_roundtrip() {
550        let mut nodemap = build_bitfield_nodemap();
551        let io = MockIo::with_registers(&[(0x5004, vec![0b1010_0000, 0b0000_0000])]);
552
553        let value = nodemap
554            .get_integer("BeBits", &io)
555            .expect("read big-endian bits");
556        assert_eq!(value, 0b101);
557
558        nodemap
559            .set_integer("BeBits", 0b010, &io)
560            .expect("write big-endian bits");
561        let data = io.read(0x5004, 2).expect("read back register");
562        assert_eq!(data, vec![0b0100_0000, 0b0000_0000]);
563    }
564
565    #[test]
566    fn bitfield_boolean_toggle() {
567        let mut nodemap = build_bitfield_nodemap();
568        let io = MockIo::with_registers(&[(0x5006, vec![0x00, 0x20, 0x00, 0x00])]);
569
570        assert!(nodemap.get_bool("PackedFlag", &io).expect("read flag"));
571
572        nodemap
573            .set_bool("PackedFlag", false, &io)
574            .expect("clear flag");
575        let data = io.read(0x5006, 4).expect("read cleared");
576        assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]);
577
578        nodemap.set_bool("PackedFlag", true, &io).expect("set flag");
579        let data = io.read(0x5006, 4).expect("read set");
580        assert_eq!(data, vec![0x00, 0x20, 0x00, 0x00]);
581    }
582
583    #[test]
584    fn bitfield_value_too_wide() {
585        let mut nodemap = build_bitfield_nodemap();
586        let io = MockIo::with_registers(&[(0x5004, vec![0x00, 0x00])]);
587
588        let err = nodemap
589            .set_integer("BeBits", 8, &io)
590            .expect_err("value too wide");
591        match err {
592            GenApiError::ValueTooWide {
593                name, bit_length, ..
594            } => {
595                assert_eq!(name, "BeBits");
596                assert_eq!(bit_length, 3);
597            }
598            other => panic!("unexpected error: {other:?}"),
599        }
600    }
601    #[test]
602    fn swissknife_evaluates_and_invalidates() {
603        let mut nodemap = build_swissknife_nodemap();
604        let io = MockIo::with_registers(&[
605            (0x3000, i64_to_bytes("GainRaw", 100, 4).unwrap()),
606            (0x3008, i64_to_bytes("Offset", 3, 4).unwrap()),
607            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
608        ]);
609
610        let value = nodemap
611            .get_float("ComputedGain", &io)
612            .expect("compute gain");
613        assert!((value - 53.0).abs() < 1e-6);
614
615        nodemap
616            .set_integer("GainRaw", 120, &io)
617            .expect("update raw gain");
618        let updated = nodemap
619            .get_float("ComputedGain", &io)
620            .expect("recompute gain");
621        assert!((updated - 63.0).abs() < 1e-6);
622    }
623
624    #[test]
625    fn swissknife_integer_rounding_and_unary() {
626        let mut nodemap = build_swissknife_nodemap();
627        let io = MockIo::with_registers(&[
628            (0x3000, i64_to_bytes("GainRaw", 5, 4).unwrap()),
629            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
630            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
631        ]);
632
633        let divided = nodemap
634            .get_integer("DivideInt", &io)
635            .expect("integer division");
636        assert_eq!(divided, 2);
637
638        nodemap
639            .set_integer("GainRaw", 3, &io)
640            .expect("update gain raw");
641        let unary = nodemap.get_integer("Unary", &io).expect("unary expression");
642        assert_eq!(unary, 7);
643    }
644
645    #[test]
646    fn swissknife_unknown_variable_error() {
647        const XML: &str = r#"
648            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
649                <Integer Name="A">
650                    <Address>0x2000</Address>
651                    <Length>4</Length>
652                    <AccessMode>RW</AccessMode>
653                    <Min>0</Min>
654                    <Max>100</Max>
655                </Integer>
656                <SwissKnife Name="Bad">
657                    <Expression>A + Missing</Expression>
658                    <pVariable Name="A">A</pVariable>
659                </SwissKnife>
660            </RegisterDescription>
661        "#;
662
663        let model = viva_genapi_xml::parse(XML).expect("parse invalid swissknife");
664        let err = NodeMap::try_from_xml(model).expect_err("unknown variable");
665        match err {
666            GenApiError::UnknownVariable { name, var } => {
667                assert_eq!(name, "Bad");
668                assert_eq!(var, "Missing");
669            }
670            other => panic!("unexpected error: {other:?}"),
671        }
672    }
673
674    #[test]
675    fn swissknife_division_by_zero() {
676        let nodemap = build_swissknife_nodemap();
677        let io = MockIo::with_registers(&[
678            (0x3000, i64_to_bytes("GainRaw", 10, 4).unwrap()),
679            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
680            (0x3010, i64_to_bytes("B", 0, 4).unwrap()),
681        ]);
682
683        let err = nodemap
684            .get_float("DivideByZero", &io)
685            .expect_err("division by zero");
686        match err {
687            GenApiError::ExprEval { name, msg } => {
688                assert_eq!(name, "DivideByZero");
689                assert_eq!(msg, "division by zero");
690            }
691            other => panic!("unexpected error: {other:?}"),
692        }
693    }
694
695    // -----------------------------------------------------------------------
696    // nodes_at_visibility
697    // -----------------------------------------------------------------------
698
699    const VISIBILITY_FIXTURE: &str = r#"
700        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
701            <Integer Name="BeginnerNode">
702                <Address>0x6000</Address>
703                <Length>4</Length>
704                <AccessMode>RW</AccessMode>
705                <Visibility>Beginner</Visibility>
706                <Min>0</Min>
707                <Max>100</Max>
708            </Integer>
709            <Integer Name="ExpertNode">
710                <Address>0x6010</Address>
711                <Length>4</Length>
712                <AccessMode>RW</AccessMode>
713                <Visibility>Expert</Visibility>
714                <Min>0</Min>
715                <Max>100</Max>
716            </Integer>
717            <Integer Name="GuruNode">
718                <Address>0x6020</Address>
719                <Length>4</Length>
720                <AccessMode>RW</AccessMode>
721                <Visibility>Guru</Visibility>
722                <Min>0</Min>
723                <Max>100</Max>
724            </Integer>
725            <Integer Name="InvisibleNode">
726                <Address>0x6030</Address>
727                <Length>4</Length>
728                <AccessMode>RW</AccessMode>
729                <Visibility>Invisible</Visibility>
730                <Min>0</Min>
731                <Max>100</Max>
732            </Integer>
733        </RegisterDescription>
734    "#;
735
736    #[test]
737    fn nodes_at_visibility_beginner_returns_only_beginner() {
738        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
739        let nodemap = NodeMap::from(model);
740
741        let visible = nodemap.nodes_at_visibility(Visibility::Beginner);
742        assert!(
743            visible.contains(&"BeginnerNode"),
744            "Beginner node must be visible at Beginner level"
745        );
746        assert!(
747            !visible.contains(&"ExpertNode"),
748            "Expert node must NOT be visible at Beginner level"
749        );
750        assert!(
751            !visible.contains(&"GuruNode"),
752            "Guru node must NOT be visible at Beginner level"
753        );
754        assert!(
755            !visible.contains(&"InvisibleNode"),
756            "Invisible node must NOT be visible at Beginner level"
757        );
758    }
759
760    #[test]
761    fn nodes_at_visibility_guru_includes_beginner_and_expert_but_not_invisible() {
762        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
763        let nodemap = NodeMap::from(model);
764
765        let visible = nodemap.nodes_at_visibility(Visibility::Guru);
766        assert!(
767            visible.contains(&"BeginnerNode"),
768            "Beginner node must be visible at Guru level"
769        );
770        assert!(
771            visible.contains(&"ExpertNode"),
772            "Expert node must be visible at Guru level"
773        );
774        assert!(
775            visible.contains(&"GuruNode"),
776            "Guru node must be visible at Guru level"
777        );
778        assert!(
779            !visible.contains(&"InvisibleNode"),
780            "Invisible node must NOT be visible at Guru level"
781        );
782    }
783}