1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod 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; 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 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}