1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod bitops;
5mod conversions;
6mod error;
7mod io;
8mod nodemap;
9mod nodes;
10pub mod 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 RegisterNode, Representation, SkNode, Visibility,
18};
19pub use swissknife::{AstNode, EvalMode, Value};
20pub use viva_genapi_xml::{AccessMode, SkOutput, SkippedNode};
21
22#[cfg(test)]
23mod tests {
24 use std::cell::RefCell;
25 use std::collections::HashMap;
26
27 use crate::conversions::{bytes_to_i64, i64_to_bytes};
28 use crate::{AccessMode, GenApiError, NodeMap, RegisterIo, Visibility};
29 use viva_genapi_xml::Sign;
30
31 const FIXTURE: &str = r#"
32 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
33 <Integer Name="Width">
34 <Address>0x100</Address>
35 <Length>4</Length>
36 <AccessMode>RW</AccessMode>
37 <Min>16</Min>
38 <Max>4096</Max>
39 <Inc>2</Inc>
40 </Integer>
41 <Float Name="ExposureTime">
42 <Address>0x200</Address>
43 <Length>4</Length>
44 <AccessMode>RW</AccessMode>
45 <Min>10.0</Min>
46 <Max>100000.0</Max>
47 <Scale>1/1000</Scale>
48 </Float>
49 <Enumeration Name="GainSelector">
50 <Address>0x300</Address>
51 <Length>2</Length>
52 <AccessMode>RW</AccessMode>
53 <EnumEntry Name="All" Value="0" />
54 <EnumEntry Name="Red" Value="1" />
55 <EnumEntry Name="Blue" Value="2" />
56 </Enumeration>
57 <Integer Name="Gain">
58 <Length>2</Length>
59 <AccessMode>RW</AccessMode>
60 <Min>0</Min>
61 <Max>48</Max>
62 <pSelected>GainSelector</pSelected>
63 <Selected>All</Selected>
64 <Address>0x310</Address>
65 <Selected>Red</Selected>
66 <Address>0x314</Address>
67 <Selected>Blue</Selected>
68 </Integer>
69 <Boolean Name="GammaEnable">
70 <Address>0x400</Address>
71 <Length>1</Length>
72 <AccessMode>RW</AccessMode>
73 </Boolean>
74 <Command Name="AcquisitionStart">
75 <Address>0x500</Address>
76 <Length>4</Length>
77 </Command>
78 </RegisterDescription>
79 "#;
80
81 const INDIRECT_FIXTURE: &str = r#"
82 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
83 <Integer Name="RegAddr">
84 <Address>0x2000</Address>
85 <Length>4</Length>
86 <AccessMode>RW</AccessMode>
87 <Min>0</Min>
88 <Max>65535</Max>
89 </Integer>
90 <Integer Name="Gain">
91 <pAddress>RegAddr</pAddress>
92 <Length>4</Length>
93 <AccessMode>RW</AccessMode>
94 <Min>0</Min>
95 <Max>255</Max>
96 </Integer>
97 </RegisterDescription>
98 "#;
99
100 const ENUM_PVALUE_FIXTURE: &str = r#"
101 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
102 <Enumeration Name="Mode">
103 <Address>0x4000</Address>
104 <Length>4</Length>
105 <AccessMode>RW</AccessMode>
106 <EnumEntry Name="Fixed10">
107 <Value>10</Value>
108 </EnumEntry>
109 <EnumEntry Name="DynFromReg">
110 <pValue>RegModeVal</pValue>
111 </EnumEntry>
112 </Enumeration>
113 <Integer Name="RegModeVal">
114 <Address>0x4100</Address>
115 <Length>4</Length>
116 <AccessMode>RW</AccessMode>
117 <Min>0</Min>
118 <Max>65535</Max>
119 </Integer>
120 </RegisterDescription>
121 "#;
122
123 const BITFIELD_FIXTURE: &str = r#"
124 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
125 <Integer Name="LeByte">
126 <Address>0x5000</Address>
127 <Length>4</Length>
128 <AccessMode>RW</AccessMode>
129 <Min>0</Min>
130 <Max>65535</Max>
131 <Mask>0x0000FF00</Mask>
132 </Integer>
133 <Integer Name="BeBits">
134 <Address>0x5004</Address>
135 <Length>2</Length>
136 <AccessMode>RW</AccessMode>
137 <Min>0</Min>
138 <Max>15</Max>
139 <Lsb>13</Lsb>
140 <Msb>15</Msb>
141 <Endianness>BigEndian</Endianness>
142 </Integer>
143 <Boolean Name="PackedFlag">
144 <Address>0x5006</Address>
145 <Length>4</Length>
146 <AccessMode>RW</AccessMode>
147 <Bit>13</Bit>
148 </Boolean>
149 </RegisterDescription>
150 "#;
151
152 const SWISSKNIFE_FIXTURE: &str = r#"
153 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
154 <Integer Name="GainRaw">
155 <Address>0x3000</Address>
156 <Length>4</Length>
157 <AccessMode>RW</AccessMode>
158 <Min>0</Min>
159 <Max>1000</Max>
160 </Integer>
161 <Float Name="Offset">
162 <Address>0x3008</Address>
163 <Length>4</Length>
164 <AccessMode>RW</AccessMode>
165 <Min>-100.0</Min>
166 <Max>100.0</Max>
167 <Scale>1</Scale>
168 </Float>
169 <Integer Name="B">
170 <Address>0x3010</Address>
171 <Length>4</Length>
172 <AccessMode>RW</AccessMode>
173 <Min>-1000</Min>
174 <Max>1000</Max>
175 </Integer>
176 <SwissKnife Name="ComputedGain">
177 <Formula>(GainRaw * 0.5) + Offset</Formula>
178 <pVariable Name="GainRaw">GainRaw</pVariable>
179 <pVariable Name="Offset">Offset</pVariable>
180 </SwissKnife>
181 <IntSwissKnife Name="DivideInt">
182 <Formula>GainRaw / 3</Formula>
183 <pVariable Name="GainRaw">GainRaw</pVariable>
184 </IntSwissKnife>
185 <IntSwissKnife Name="Unary">
186 <Formula>-GainRaw + 10</Formula>
187 <pVariable Name="GainRaw">GainRaw</pVariable>
188 </IntSwissKnife>
189 <SwissKnife Name="DivideByZero">
190 <Formula>GainRaw / B</Formula>
191 <pVariable Name="GainRaw">GainRaw</pVariable>
192 <pVariable Name="B">B</pVariable>
193 </SwissKnife>
194 </RegisterDescription>
195 "#;
196
197 #[derive(Default)]
198 struct MockIo {
199 regs: RefCell<HashMap<u64, Vec<u8>>>,
200 reads: RefCell<HashMap<u64, usize>>,
201 }
202
203 impl MockIo {
204 fn with_registers(entries: &[(u64, Vec<u8>)]) -> Self {
205 let mut regs = HashMap::new();
206 for (addr, data) in entries {
207 regs.insert(*addr, data.clone());
208 }
209 MockIo {
210 regs: RefCell::new(regs),
211 reads: RefCell::new(HashMap::new()),
212 }
213 }
214
215 fn read_count(&self, addr: u64) -> usize {
216 *self.reads.borrow().get(&addr).unwrap_or(&0)
217 }
218 }
219
220 impl RegisterIo for MockIo {
221 fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
222 let mut reads = self.reads.borrow_mut();
223 *reads.entry(addr).or_default() += 1;
224 let regs = self.regs.borrow();
225 let data = regs
226 .get(&addr)
227 .ok_or_else(|| GenApiError::Io(format!("read miss at 0x{addr:08X}")))?;
228 if data.len() != len {
229 return Err(GenApiError::Io(format!(
230 "length mismatch at 0x{addr:08X}: expected {len}, have {}",
231 data.len()
232 )));
233 }
234 Ok(data.clone())
235 }
236
237 fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
238 self.regs.borrow_mut().insert(addr, data.to_vec());
239 Ok(())
240 }
241 }
242
243 fn build_nodemap() -> NodeMap {
244 let model = viva_genapi_xml::parse(FIXTURE).expect("parse fixture");
245 NodeMap::try_from_xml(model).expect("build nodemap")
246 }
247
248 #[test]
256 fn an_unsupported_node_type_is_visible_in_the_nodemap() {
257 const WITH_UNKNOWN: &str = r#"
260 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
261 <Integer Name="Width">
262 <Address>0x100</Address>
263 <Length>4</Length>
264 <AccessMode>RW</AccessMode>
265 </Integer>
266 <ConfRom Name="DeviceConfRom">
267 <Address>0x2000</Address>
268 <Length>512</Length>
269 </ConfRom>
270 </RegisterDescription>
271 "#;
272 let model = viva_genapi_xml::parse(WITH_UNKNOWN).expect("parse");
273 assert_eq!(model.skipped.len(), 1, "XML layer records the unknown tag");
274
275 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
276 let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 7])]);
277 assert_eq!(nodemap.get_integer("Width", &io).expect("read Width"), 7);
278
279 let skipped = nodemap.skipped();
280 assert_eq!(skipped.len(), 1, "and the nodemap carries it forward");
281 assert_eq!(skipped[0].tag, "ConfRom");
282 assert_eq!(skipped[0].name.as_deref(), Some("DeviceConfRom"));
283 }
284
285 const REGISTER_FIXTURE: &str = r#"
289 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
290 <Register Name="FileAccessBuffer">
291 <Address>0x2000</Address>
292 <Length>8</Length>
293 <AccessMode>RW</AccessMode>
294 </Register>
295 <Register Name="DeviceSerialBlock">
296 <Address>0x3000</Address>
297 <Length>4</Length>
298 <AccessMode>RO</AccessMode>
299 <pPort>Device</pPort>
300 </Register>
301 <Register Name="ChunkMeasurementResults">
302 <Address>0x0</Address>
303 <Length>4</Length>
304 <AccessMode>RO</AccessMode>
305 <pPort>Chunk4007</pPort>
306 </Register>
307 <Register Name="DynamicBlock">
308 <Address>0x4000</Address>
309 <pLength>BlockLength</pLength>
310 </Register>
311 <StringReg Name="DeviceVendorName">
312 <Address>0x5000</Address>
313 <Length>4</Length>
314 <AccessMode>RO</AccessMode>
315 </StringReg>
316 </RegisterDescription>
317 "#;
318
319 fn build_register_nodemap() -> NodeMap {
320 let model = viva_genapi_xml::parse(REGISTER_FIXTURE).expect("parse register fixture");
321 NodeMap::try_from_xml(model).expect("build nodemap")
322 }
323
324 #[test]
325 fn register_reads_and_writes_raw_bytes() {
326 let nodemap = build_register_nodemap();
327 let payload = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
328 let io = MockIo::with_registers(&[(0x2000, payload.clone())]);
329
330 assert_eq!(
331 nodemap
332 .get_register("FileAccessBuffer", &io)
333 .expect("read register"),
334 payload
335 );
336
337 let replacement = vec![9u8; 8];
338 nodemap
339 .set_register("FileAccessBuffer", &replacement, &io)
340 .expect("write register");
341 assert_eq!(
342 nodemap
343 .get_register("FileAccessBuffer", &io)
344 .expect("re-read register"),
345 replacement
346 );
347 }
348
349 #[test]
355 fn register_write_of_the_wrong_length_is_refused() {
356 let nodemap = build_register_nodemap();
357 let io = MockIo::with_registers(&[(0x2000, vec![0u8; 8])]);
358
359 let err = nodemap
360 .set_register("FileAccessBuffer", &[1, 2, 3], &io)
361 .expect_err("a 3-byte write into an 8-byte register must fail");
362 assert!(
363 matches!(err, GenApiError::Range(_)),
364 "expected a range error, got {err:?}"
365 );
366 assert_eq!(
367 io.read(0x2000, 8).expect("register untouched"),
368 vec![0u8; 8],
369 "the refused write must not have reached the device"
370 );
371 }
372
373 #[test]
380 fn register_on_a_non_device_port_is_listed_but_not_readable() {
381 let nodemap = build_register_nodemap();
382 let io = MockIo::with_registers(&[(0x0, vec![1, 2, 3, 4])]);
383
384 assert!(
385 nodemap.node_names().any(|n| n == "ChunkMeasurementResults"),
386 "the node must still be visible for introspection"
387 );
388
389 let err = nodemap
390 .get_register("ChunkMeasurementResults", &io)
391 .expect_err("a chunk-port register must not be read through the device port");
392 let text = err.to_string();
393 assert!(
394 text.contains("Chunk4007") && text.contains("GA-12"),
395 "the error must name the port and the reason: {text}"
396 );
397
398 let io = MockIo::with_registers(&[(0x3000, vec![5, 6, 7, 8])]);
400 assert_eq!(
401 nodemap
402 .get_register("DeviceSerialBlock", &io)
403 .expect("explicit Device port reads"),
404 vec![5, 6, 7, 8]
405 );
406 }
407
408 #[test]
410 fn register_address_resolves_for_a_register_node() {
411 let nodemap = build_register_nodemap();
412 let io = MockIo::default();
413 assert_eq!(
414 nodemap
415 .register_address("FileAccessBuffer", &io)
416 .expect("resolve address"),
417 (0x2000, 8)
418 );
419 }
420
421 #[test]
422 fn register_accessors_reject_the_wrong_node_type() {
423 let nodemap = build_register_nodemap();
424 let io = MockIo::with_registers(&[(0x5000, b"AVT\0".to_vec())]);
425
426 assert!(matches!(
427 nodemap.get_register("DeviceVendorName", &io),
428 Err(GenApiError::Type(_))
429 ));
430 assert!(matches!(
431 nodemap.get_register("NoSuchNode", &io),
432 Err(GenApiError::NodeNotFound(_))
433 ));
434 }
435
436 #[test]
441 fn a_register_with_p_length_is_skipped_and_says_why() {
442 let nodemap = build_register_nodemap();
443 let skipped = nodemap.skipped();
444
445 let entry = skipped
446 .iter()
447 .find(|s| s.name.as_deref() == Some("DynamicBlock"))
448 .expect("a <pLength> register must be recorded, not silently dropped");
449 assert_eq!(entry.tag, "Register");
450 assert!(
451 entry.error.contains("<pLength>"),
452 "the skip reason must name <pLength>: {}",
453 entry.error
454 );
455
456 assert_eq!(nodemap.node_names().count(), 4);
458 }
459
460 #[test]
462 fn elements_without_a_name_are_not_reported_as_skipped() {
463 const WITH_PORT: &str = r#"
464 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
465 <Port Name="Device"/>
466 <Group Comment="Standard">
467 <Integer Name="Width">
468 <Address>0x100</Address>
469 <Length>4</Length>
470 <AccessMode>RW</AccessMode>
471 </Integer>
472 </Group>
473 </RegisterDescription>
474 "#;
475 let model = viva_genapi_xml::parse(WITH_PORT).expect("parse");
476 assert!(model.skipped.is_empty(), "{:?}", model.skipped);
477 assert_eq!(model.nodes.len(), 1);
478 }
479
480 fn build_indirect_nodemap() -> NodeMap {
481 let model = viva_genapi_xml::parse(INDIRECT_FIXTURE).expect("parse indirect fixture");
482 NodeMap::try_from_xml(model).expect("build nodemap")
483 }
484
485 fn build_enum_pvalue_nodemap() -> NodeMap {
486 let model = viva_genapi_xml::parse(ENUM_PVALUE_FIXTURE).expect("parse enum pvalue fixture");
487 NodeMap::try_from_xml(model).expect("build nodemap")
488 }
489
490 fn build_bitfield_nodemap() -> NodeMap {
491 let model = viva_genapi_xml::parse(BITFIELD_FIXTURE).expect("parse bitfield fixture");
492 NodeMap::try_from_xml(model).expect("build nodemap")
493 }
494
495 fn build_swissknife_nodemap() -> NodeMap {
496 let model = viva_genapi_xml::parse(SWISSKNIFE_FIXTURE).expect("parse swissknife fixture");
497 NodeMap::try_from_xml(model).expect("build nodemap")
498 }
499
500 #[test]
501 fn integer_roundtrip_and_cache() {
502 let mut nodemap = build_nodemap();
503 let io = MockIo::with_registers(&[(0x100, vec![0, 0, 4, 0])]);
504 let width = nodemap.get_integer("Width", &io).expect("read width");
505 assert_eq!(width, 1024);
506 assert_eq!(io.read_count(0x100), 1);
507 let width_again = nodemap.get_integer("Width", &io).expect("cached width");
508 assert_eq!(width_again, 1024);
509 assert_eq!(io.read_count(0x100), 1, "cached value should be reused");
510 nodemap
511 .set_integer("Width", 1030, &io)
512 .expect("write width");
513 let width = nodemap
514 .get_integer("Width", &io)
515 .expect("read updated width");
516 assert_eq!(width, 1030);
517 assert_eq!(io.read_count(0x100), 1, "write should update cache");
518 }
519
520 const IEEE754_FIXTURE: &str = r#"
521 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
522 <FloatReg Name="FrameRate">
523 <Address>0x100</Address>
524 <Length>4</Length>
525 <AccessMode>RW</AccessMode>
526 <Min>0.0</Min>
527 <Max>1000.0</Max>
528 <Endianess>BigEndian</Endianess>
529 </FloatReg>
530 <Float Name="ExposureUs">
531 <Address>0x110</Address>
532 <Length>8</Length>
533 <AccessMode>RW</AccessMode>
534 <Min>0.0</Min>
535 <Max>1000000.0</Max>
536 <Endianess>BigEndian</Endianess>
537 </Float>
538 </RegisterDescription>
539 "#;
540
541 fn build_ieee754_nodemap() -> NodeMap {
542 NodeMap::try_from_xml(viva_genapi_xml::parse(IEEE754_FIXTURE).expect("parse ieee754"))
543 .expect("build nodemap")
544 }
545
546 #[test]
547 fn float_ieee754_f32_roundtrip() {
548 let mut nodemap = build_ieee754_nodemap();
549 let io = MockIo::with_registers(&[(0x100, 30.0f32.to_be_bytes().to_vec())]);
550 let v = nodemap.get_float("FrameRate", &io).expect("read rate");
551 assert!((v - 30.0).abs() < 1e-3, "got {v}");
552
553 nodemap
554 .set_float("FrameRate", 42.5, &io)
555 .expect("write rate");
556 let raw = io.read(0x100, 4).expect("read back");
557 assert_eq!(raw, 42.5f32.to_be_bytes());
558 }
559
560 #[test]
561 fn float_ieee754_f64_heuristic_roundtrip() {
562 let mut nodemap = build_ieee754_nodemap();
563 let io = MockIo::with_registers(&[(0x110, 6000.0f64.to_be_bytes().to_vec())]);
564 let v = nodemap.get_float("ExposureUs", &io).expect("read exposure");
565 assert!((v - 6000.0).abs() < 1e-9, "got {v}");
566
567 nodemap
568 .set_float("ExposureUs", 5000.0, &io)
569 .expect("write exposure");
570 let raw = io.read(0x110, 8).expect("read back");
571 assert_eq!(raw, 5000.0f64.to_be_bytes());
572 }
573
574 #[test]
575 fn float_scaled_integer_preserved() {
576 let nodemap = build_nodemap();
579 let raw = 50_000i64;
580 let io = MockIo::with_registers(&[(
581 0x200,
582 i64_to_bytes("ExposureTime", raw, 4, Sign::Signed).unwrap(),
583 )]);
584 let exposure = nodemap
585 .get_float("ExposureTime", &io)
586 .expect("read exposure");
587 assert!((exposure - 50.0).abs() < 1e-6);
588 }
589
590 const PREDICATE_FIXTURE: &str = r#"
591 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
592 <IntReg Name="CtrlReg">
593 <Address>0x400</Address>
594 <Length>4</Length>
595 <AccessMode>RW</AccessMode>
596 <Sign>Unsigned</Sign>
597 <Endianess>BigEndian</Endianess>
598 </IntReg>
599 <IntSwissKnife Name="GateImplemented">
600 <Formula>CTRL & 1</Formula>
601 <pVariable Name="CTRL">CtrlReg</pVariable>
602 <Output>Integer</Output>
603 </IntSwissKnife>
604 <IntSwissKnife Name="GateLocked">
605 <Formula>(CTRL & 2) / 2</Formula>
606 <pVariable Name="CTRL">CtrlReg</pVariable>
607 <Output>Integer</Output>
608 </IntSwissKnife>
609 <IntSwissKnife Name="Entry8Implemented">
610 <Formula>(CTRL & 4) / 4</Formula>
611 <pVariable Name="CTRL">CtrlReg</pVariable>
612 <Output>Integer</Output>
613 </IntSwissKnife>
614 <Integer Name="Gated">
615 <Address>0x410</Address>
616 <Length>4</Length>
617 <AccessMode>RW</AccessMode>
618 <Min>0</Min>
619 <Max>255</Max>
620 <Sign>Unsigned</Sign>
621 <Endianess>BigEndian</Endianess>
622 <pIsImplemented>GateImplemented</pIsImplemented>
623 <pIsLocked>GateLocked</pIsLocked>
624 </Integer>
625 <Enumeration Name="PixelFormat">
626 <EnumEntry Name="Mono8"><Value>1</Value></EnumEntry>
627 <EnumEntry Name="Mono16">
628 <Value>2</Value>
629 <pIsImplemented>Entry8Implemented</pIsImplemented>
630 </EnumEntry>
631 <pValue>PixelFormatReg</pValue>
632 </Enumeration>
633 <IntReg Name="PixelFormatReg">
634 <Address>0x420</Address>
635 <Length>4</Length>
636 <AccessMode>RW</AccessMode>
637 <Sign>Unsigned</Sign>
638 <Endianess>BigEndian</Endianess>
639 </IntReg>
640 </RegisterDescription>
641 "#;
642
643 fn build_predicate_nodemap() -> NodeMap {
644 NodeMap::try_from_xml(
645 viva_genapi_xml::parse(PREDICATE_FIXTURE).expect("parse predicate fixture"),
646 )
647 .expect("build nodemap")
648 }
649
650 fn predicate_io(ctrl: u32) -> MockIo {
651 MockIo::with_registers(&[
652 (0x400, ctrl.to_be_bytes().to_vec()),
653 (0x410, 0u32.to_be_bytes().to_vec()),
654 (0x420, 1u32.to_be_bytes().to_vec()),
655 ])
656 }
657
658 #[test]
659 fn predicate_is_implemented_defaults_true() {
660 let nodemap = build_predicate_nodemap();
661 let io = predicate_io(0);
662 assert!(nodemap.is_implemented("CtrlReg", &io).unwrap());
664 }
665
666 #[test]
667 fn predicate_is_implemented_follows_gate() {
668 let nm0 = build_predicate_nodemap();
669 let io0 = predicate_io(0);
670 assert!(!nm0.is_implemented("Gated", &io0).unwrap());
671 let nm1 = build_predicate_nodemap();
672 let io1 = predicate_io(1);
673 assert!(nm1.is_implemented("Gated", &io1).unwrap());
674 }
675
676 #[test]
677 fn predicate_is_available_chains_implemented() {
678 let nm0 = build_predicate_nodemap();
679 let io0 = predicate_io(0);
680 assert!(!nm0.is_available("Gated", &io0).unwrap());
681 let nm1 = build_predicate_nodemap();
682 let io1 = predicate_io(1);
683 assert!(nm1.is_available("Gated", &io1).unwrap());
684 }
685
686 #[test]
687 fn predicate_effective_access_mode_locked_downgrade() {
688 let nodemap = build_predicate_nodemap();
689 let io = predicate_io(0b11);
691 let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
692 assert_eq!(mode, AccessMode::RO);
693 }
694
695 #[test]
696 fn write_to_a_locked_node_is_refused_locally() {
697 let mut nodemap = build_predicate_nodemap();
701 let io = predicate_io(0b11); let err = nodemap
703 .set_integer("Gated", 7, &io)
704 .expect_err("a locked node must not be written");
705 match err {
706 GenApiError::Locked { name, locked_by } => {
707 assert_eq!(name, "Gated");
708 assert_eq!(locked_by, "GateLocked");
710 }
711 other => panic!("expected Locked, got {other:?}"),
712 }
713 }
714
715 #[test]
716 fn write_to_an_unlocked_node_still_succeeds() {
717 let mut nodemap = build_predicate_nodemap();
718 let io = predicate_io(0b01); nodemap
720 .set_integer("Gated", 7, &io)
721 .expect("an unlocked RW node must still be writable");
722 }
723
724 #[test]
725 fn write_to_an_unimplemented_node_reports_unavailable_not_locked() {
726 let mut nodemap = build_predicate_nodemap();
729 let io = predicate_io(0b00); let err = nodemap
731 .set_integer("Gated", 7, &io)
732 .expect_err("an unavailable node must not be written");
733 assert!(
734 matches!(err, GenApiError::Unavailable(ref n) if n == "Gated"),
735 "expected Unavailable, got {err:?}"
736 );
737 }
738
739 #[test]
740 fn predicate_effective_access_mode_rw_when_unlocked() {
741 let nodemap = build_predicate_nodemap();
742 let io = predicate_io(0b01);
744 let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
745 assert_eq!(mode, AccessMode::RW);
746 }
747
748 #[test]
749 fn predicate_effective_access_mode_na_for_unavailable() {
750 let nodemap = build_predicate_nodemap();
751 let io = predicate_io(0);
753 let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
754 assert_eq!(mode, AccessMode::RO);
755 }
756
757 #[test]
758 fn predicate_available_enum_entries_filters() {
759 let nodemap = build_predicate_nodemap();
760 let io = predicate_io(0);
762 let entries = nodemap
763 .available_enum_entries("PixelFormat", &io)
764 .expect("enum entries");
765 assert_eq!(entries, vec!["Mono8".to_string()]);
766 }
767
768 #[test]
769 fn predicate_available_enum_entries_full_when_allowed() {
770 let nodemap = build_predicate_nodemap();
771 let io = predicate_io(0b100);
773 let mut entries = nodemap
774 .available_enum_entries("PixelFormat", &io)
775 .expect("enum entries");
776 entries.sort();
777 assert_eq!(entries, vec!["Mono16".to_string(), "Mono8".to_string()]);
778 }
779
780 #[test]
781 fn predicate_available_enum_entries_fallback_to_static() {
782 let xml = r#"
784 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
785 <Enumeration Name="Mode">
786 <EnumEntry Name="A"><Value>0</Value></EnumEntry>
787 <EnumEntry Name="B"><Value>1</Value></EnumEntry>
788 <pValue>ModeReg</pValue>
789 </Enumeration>
790 <IntReg Name="ModeReg">
791 <Address>0x500</Address>
792 <Length>4</Length>
793 <AccessMode>RW</AccessMode>
794 <Sign>Unsigned</Sign>
795 <Endianess>BigEndian</Endianess>
796 </IntReg>
797 </RegisterDescription>
798 "#;
799 let nodemap =
800 NodeMap::try_from_xml(viva_genapi_xml::parse(xml).unwrap()).expect("build nodemap");
801 let io = MockIo::with_registers(&[(0x500, 0u32.to_be_bytes().to_vec())]);
802 let mut entries = nodemap.available_enum_entries("Mode", &io).unwrap();
803 entries.sort();
804 assert_eq!(entries, vec!["A".to_string(), "B".to_string()]);
805 }
806
807 #[test]
808 fn float_conversion_roundtrip() {
809 let mut nodemap = build_nodemap();
810 let raw = 50_000i64; let io = MockIo::with_registers(&[(
812 0x200,
813 i64_to_bytes("ExposureTime", raw, 4, Sign::Signed).unwrap(),
814 )]);
815 let exposure = nodemap
816 .get_float("ExposureTime", &io)
817 .expect("read exposure");
818 assert!((exposure - 50.0).abs() < 1e-6);
819 nodemap
820 .set_float("ExposureTime", 75.0, &io)
821 .expect("write exposure");
822 let raw_back =
823 bytes_to_i64("ExposureTime", &io.read(0x200, 4).unwrap(), Sign::Signed).unwrap();
824 assert_eq!(raw_back, 75_000);
825 }
826
827 #[test]
828 fn selector_address_switching() {
829 let mut nodemap = build_nodemap();
830 let io = MockIo::with_registers(&[
831 (
832 0x300,
833 i64_to_bytes("GainSelector", 0, 2, Sign::Signed).unwrap(),
834 ),
835 (0x310, i64_to_bytes("Gain", 10, 2, Sign::Signed).unwrap()),
836 (0x314, i64_to_bytes("Gain", 24, 2, Sign::Signed).unwrap()),
837 ]);
838
839 let gain_all = nodemap.get_integer("Gain", &io).expect("gain for All");
840 assert_eq!(gain_all, 10);
841 assert_eq!(io.read_count(0x310), 1);
842 assert_eq!(io.read_count(0x314), 0);
843
844 io.write(0x314, &i64_to_bytes("Gain", 32, 2, Sign::Signed).unwrap())
845 .expect("update red gain");
846 nodemap
847 .set_enum("GainSelector", "Red", &io)
848 .expect("set selector to red");
849 let gain_red = nodemap.get_integer("Gain", &io).expect("gain for Red");
850 assert_eq!(gain_red, 32);
851 assert_eq!(
852 io.read_count(0x310),
853 1,
854 "previous address should not be reread"
855 );
856 assert_eq!(io.read_count(0x314), 1);
857
858 let gain_red_cached = nodemap.get_integer("Gain", &io).expect("cached red");
859 assert_eq!(gain_red_cached, 32);
860 assert_eq!(io.read_count(0x314), 1, "selector cache should be reused");
861
862 nodemap
863 .set_enum("GainSelector", "Blue", &io)
864 .expect("set selector to blue");
865 let err = nodemap.get_integer("Gain", &io).unwrap_err();
866 match err {
867 GenApiError::Unavailable(msg) => {
868 assert!(msg.contains("GainSelector=Blue"));
869 }
870 other => panic!("unexpected error: {other:?}"),
871 }
872 assert_eq!(
873 io.read_count(0x314),
874 1,
875 "no read expected for missing mapping"
876 );
877
878 io.write(0x310, &i64_to_bytes("Gain", 12, 2, Sign::Signed).unwrap())
879 .expect("update all gain");
880 nodemap
881 .set_enum("GainSelector", "All", &io)
882 .expect("restore selector to all");
883 let gain_all_updated = nodemap
884 .get_integer("Gain", &io)
885 .expect("gain for All again");
886 assert_eq!(gain_all_updated, 12);
887 assert_eq!(
888 io.read_count(0x310),
889 2,
890 "address switch should invalidate cache"
891 );
892 }
893
894 #[test]
895 fn range_enforcement() {
896 let mut nodemap = build_nodemap();
897 let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 16])]);
898 let err = nodemap.set_integer("Width", 17, &io).unwrap_err();
899 assert!(matches!(err, GenApiError::Range(_)));
900 }
901
902 #[test]
903 fn command_exec() {
904 let mut nodemap = build_nodemap();
905 let io = MockIo::with_registers(&[]);
906 nodemap
907 .exec_command("AcquisitionStart", &io)
908 .expect("exec command");
909 let payload = io.read(0x500, 4).expect("command write");
910 assert_eq!(payload, vec![0, 0, 0, 1]);
911 }
912
913 #[test]
914 fn indirect_address_resolution() {
915 let mut nodemap = build_indirect_nodemap();
916 let io = MockIo::with_registers(&[
917 (
918 0x2000,
919 i64_to_bytes("RegAddr", 0x3000, 4, Sign::Signed).unwrap(),
920 ),
921 (0x3000, i64_to_bytes("Gain", 123, 4, Sign::Signed).unwrap()),
922 (0x3100, i64_to_bytes("Gain", 77, 4, Sign::Signed).unwrap()),
923 ]);
924
925 let initial = nodemap.get_integer("Gain", &io).expect("read gain");
926 assert_eq!(initial, 123);
927 assert_eq!(io.read_count(0x2000), 1);
928 assert_eq!(io.read_count(0x3000), 1);
929
930 nodemap
931 .set_integer("RegAddr", 0x3100, &io)
932 .expect("set indirect address");
933 let updated = nodemap
934 .get_integer("Gain", &io)
935 .expect("read gain after change");
936 assert_eq!(updated, 77);
937 assert_eq!(io.read_count(0x2000), 1);
938 assert_eq!(io.read_count(0x3000), 1);
939 assert_eq!(io.read_count(0x3100), 1);
940 }
941
942 #[test]
959 fn indirect_bad_address() {
960 const XML: &str = r#"
961 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
962 <Integer Name="RegAddr">
963 <Address>0x2000</Address>
964 <Length>4</Length>
965 <AccessMode>RW</AccessMode>
966 <Sign>Signed</Sign>
967 <Min>-65535</Min>
968 <Max>65535</Max>
969 </Integer>
970 <Integer Name="Gain">
971 <pAddress>RegAddr</pAddress>
972 <Length>4</Length>
973 <AccessMode>RW</AccessMode>
974 <Min>0</Min>
975 <Max>255</Max>
976 </Integer>
977 </RegisterDescription>
978 "#;
979
980 let model = viva_genapi_xml::parse(XML).expect("parse");
981 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
982 let io = MockIo::with_registers(&[(
983 0x2000,
984 i64_to_bytes("RegAddr", -4, 4, Sign::Signed).unwrap(),
985 )]);
986
987 let err = nodemap.get_integer("Gain", &io).unwrap_err();
988 match err {
989 GenApiError::BadIndirectAddress { name, addr } => {
990 assert_eq!(name, "Gain");
991 assert_eq!(addr, -4);
992 }
993 other => panic!("unexpected error: {other:?}"),
994 }
995 }
996
997 #[test]
1001 fn unsigned_registers_do_not_sign_extend() {
1002 const XML: &str = r#"
1003 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1004 <Integer Name="GevCurrentIPAddress">
1005 <Address>0x1000</Address>
1006 <Length>4</Length>
1007 <AccessMode>RO</AccessMode>
1008 <Min>0</Min>
1009 <Max>4294967295</Max>
1010 </Integer>
1011 <Integer Name="TemperatureOffset">
1012 <Address>0x1004</Address>
1013 <Length>4</Length>
1014 <AccessMode>RO</AccessMode>
1015 <Sign>Signed</Sign>
1016 <Min>-2147483648</Min>
1017 <Max>2147483647</Max>
1018 </Integer>
1019 </RegisterDescription>
1020 "#;
1021
1022 let model = viva_genapi_xml::parse(XML).expect("parse");
1023 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1024 let io = MockIo::with_registers(&[
1025 (0x1000, vec![0xC0, 0xA8, 0x01, 0xA0]),
1027 (0x1004, vec![0xFF, 0xFF, 0xFF, 0xFB]),
1028 ]);
1029
1030 assert_eq!(
1031 nodemap
1032 .get_integer("GevCurrentIPAddress", &io)
1033 .expect("read address"),
1034 0xC0A8_01A0
1035 );
1036 assert_eq!(
1037 nodemap
1038 .get_integer("TemperatureOffset", &io)
1039 .expect("read offset"),
1040 -5
1041 );
1042 }
1043
1044 #[test]
1051 fn converter_directions_are_not_swapped() {
1052 const XML: &str = r#"
1053 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1054 <IntReg Name="ExposureTimeRaw">
1055 <Address>0x1000</Address>
1056 <Length>4</Length>
1057 <AccessMode>RW</AccessMode>
1058 </IntReg>
1059 <Converter Name="ExposureTime">
1060 <FormulaTo>FROM * 100</FormulaTo>
1061 <FormulaFrom>TO / 100</FormulaFrom>
1062 <pValue>ExposureTimeRaw</pValue>
1063 <Unit>us</Unit>
1064 </Converter>
1065 </RegisterDescription>
1066 "#;
1067
1068 let model = viva_genapi_xml::parse(XML).expect("parse");
1069 let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1070 let io = MockIo::with_registers(&[(
1071 0x1000,
1072 i64_to_bytes("ExposureTimeRaw", 5000, 4, Sign::Unsigned).unwrap(),
1073 )]);
1074
1075 let value = nodemap.get_float("ExposureTime", &io).expect("read");
1077 assert!((value - 50.0).abs() < 1e-9, "got {value}");
1078
1079 nodemap.set_float("ExposureTime", 75.0, &io).expect("write");
1081 assert_eq!(
1082 nodemap
1083 .get_integer("ExposureTimeRaw", &io)
1084 .expect("read raw back"),
1085 7500
1086 );
1087 let roundtrip = nodemap.get_float("ExposureTime", &io).expect("re-read");
1088 assert!((roundtrip - 75.0).abs() < 1e-9, "got {roundtrip}");
1089 }
1090
1091 #[test]
1094 fn int_converter_write_preserves_other_bits() {
1095 const XML: &str = r#"
1096 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1097 <IntReg Name="Binning_Reg">
1098 <Address>0x1000</Address>
1099 <Length>4</Length>
1100 <AccessMode>RW</AccessMode>
1101 </IntReg>
1102 <IntConverter Name="BinningHorizontal">
1103 <FormulaTo>FROM | (OLD & 0xffff0000)</FormulaTo>
1104 <FormulaFrom>TO & 0x0000ffff</FormulaFrom>
1105 <pVariable Name="OLD">Binning_Reg</pVariable>
1106 <pValue>Binning_Reg</pValue>
1107 </IntConverter>
1108 </RegisterDescription>
1109 "#;
1110
1111 let model = viva_genapi_xml::parse(XML).expect("parse");
1112 let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1113 let io = MockIo::with_registers(&[(
1114 0x1000,
1115 i64_to_bytes("Binning_Reg", 0x0004_0002, 4, Sign::Unsigned).unwrap(),
1116 )]);
1117
1118 assert_eq!(
1119 nodemap
1120 .get_integer("BinningHorizontal", &io)
1121 .expect("read low half"),
1122 2
1123 );
1124
1125 nodemap
1126 .set_integer("BinningHorizontal", 3, &io)
1127 .expect("write low half");
1128 assert_eq!(
1130 nodemap
1131 .get_integer("Binning_Reg", &io)
1132 .expect("read raw back"),
1133 0x0004_0003
1134 );
1135 }
1136
1137 #[test]
1146 fn sign_does_not_depend_on_min() {
1147 const XML: &str = r#"
1148 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1149 <IntReg Name="GevCurrentIPAddress">
1150 <Address>0x1000</Address>
1151 <Length>4</Length>
1152 <AccessMode>RO</AccessMode>
1153 <Sign>Unsigned</Sign>
1154 </IntReg>
1155 <IntReg Name="DeviceTemperatureRaw">
1156 <Address>0x1004</Address>
1157 <Length>4</Length>
1158 <AccessMode>RO</AccessMode>
1159 <Sign>Signed</Sign>
1160 </IntReg>
1161 <IntReg Name="ImplicitlyUnsigned">
1162 <Address>0x1008</Address>
1163 <Length>4</Length>
1164 <AccessMode>RO</AccessMode>
1165 </IntReg>
1166 </RegisterDescription>
1167 "#;
1168
1169 let model = viva_genapi_xml::parse(XML).expect("parse");
1170 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1171 let io = MockIo::with_registers(&[
1172 (0x1000, vec![0xC0, 0xA8, 0x01, 0xA0]),
1173 (0x1004, vec![0xFF, 0xFF, 0xFF, 0xFB]),
1174 (0x1008, vec![0xFF, 0xFF, 0xFF, 0xFF]),
1175 ]);
1176
1177 assert_eq!(
1178 nodemap
1179 .get_integer("GevCurrentIPAddress", &io)
1180 .expect("read address"),
1181 0xC0A8_01A0
1182 );
1183 assert_eq!(
1184 nodemap
1185 .get_integer("DeviceTemperatureRaw", &io)
1186 .expect("read temperature"),
1187 -5
1188 );
1189 assert_eq!(
1191 nodemap
1192 .get_integer("ImplicitlyUnsigned", &io)
1193 .expect("read default-signed register"),
1194 0xFFFF_FFFFu32 as i64
1195 );
1196 }
1197
1198 #[test]
1204 fn struct_entry_bits_read_unsigned() {
1205 const XML: &str = r#"
1206 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1207 <StructReg Comment="Gain Inquiry Register">
1208 <Address>0x520</Address>
1209 <Length>4</Length>
1210 <AccessMode>RO</AccessMode>
1211 <Endianess>BigEndian</Endianess>
1212 <StructEntry Name="GainPresInq_Bit"><Bit>0</Bit></StructEntry>
1213 <StructEntry Name="GainAutoInq_Bit"><Bit>6</Bit></StructEntry>
1214 </StructReg>
1215 </RegisterDescription>
1216 "#;
1217
1218 let model = viva_genapi_xml::parse(XML).expect("parse");
1219 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1220 let io = MockIo::with_registers(&[(0x520, vec![0x82, 0x00, 0x00, 0x00])]);
1224
1225 assert_eq!(
1226 nodemap
1227 .get_integer("GainPresInq_Bit", &io)
1228 .expect("read presence bit"),
1229 1
1230 );
1231 assert_eq!(
1232 nodemap
1233 .get_integer("GainAutoInq_Bit", &io)
1234 .expect("read auto bit"),
1235 1
1236 );
1237 }
1238
1239 #[test]
1243 fn address_terms_sum() {
1244 const XML: &str = r#"
1245 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1246 <Integer Name="RegBase">
1247 <Address>0x2000</Address>
1248 <Length>4</Length>
1249 <AccessMode>RW</AccessMode>
1250 <Min>0</Min>
1251 <Max>65535</Max>
1252 </Integer>
1253 <Integer Name="Gain">
1254 <pAddress>RegBase</pAddress>
1255 <Address>0x8</Address>
1256 <Length>4</Length>
1257 <AccessMode>RW</AccessMode>
1258 <Min>0</Min>
1259 <Max>255</Max>
1260 </Integer>
1261 </RegisterDescription>
1262 "#;
1263
1264 let model = viva_genapi_xml::parse(XML).expect("parse");
1265 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1266 let io = MockIo::with_registers(&[
1267 (
1268 0x2000,
1269 i64_to_bytes("RegBase", 0x3000, 4, Sign::Signed).unwrap(),
1270 ),
1271 (0x3008, i64_to_bytes("Gain", 77, 4, Sign::Signed).unwrap()),
1273 (0x3000, i64_to_bytes("Gain", 11, 4, Sign::Signed).unwrap()),
1274 ]);
1275
1276 assert_eq!(nodemap.get_integer("Gain", &io).expect("read gain"), 77);
1277
1278 assert_eq!(
1280 nodemap
1281 .register_address("Gain", &io)
1282 .expect("resolve summed address"),
1283 (0x3008, 4)
1284 );
1285 assert!(matches!(
1287 nodemap.register_address("NoSuchNode", &io),
1288 Err(GenApiError::NodeNotFound(_))
1289 ));
1290 }
1291
1292 #[test]
1295 fn p_index_scales_into_the_address() {
1296 const XML: &str = r#"
1297 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1298 <Integer Name="TriggerSelectorIdx">
1299 <Address>0x2000</Address>
1300 <Length>4</Length>
1301 <AccessMode>RW</AccessMode>
1302 <Min>0</Min>
1303 <Max>7</Max>
1304 </Integer>
1305 <Integer Name="TriggerInqDelay">
1306 <Address>0x13400</Address>
1307 <pIndex Offset="64">TriggerSelectorIdx</pIndex>
1308 <Length>4</Length>
1309 <AccessMode>RO</AccessMode>
1310 <Min>0</Min>
1311 <Max>255</Max>
1312 </Integer>
1313 </RegisterDescription>
1314 "#;
1315
1316 let model = viva_genapi_xml::parse(XML).expect("parse");
1317 let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1318 let io = MockIo::with_registers(&[
1319 (
1320 0x2000,
1321 i64_to_bytes("TriggerSelectorIdx", 2, 4, Sign::Signed).unwrap(),
1322 ),
1323 (
1324 0x13400,
1325 i64_to_bytes("TriggerInqDelay", 1, 4, Sign::Signed).unwrap(),
1326 ),
1327 (
1328 0x13480,
1329 i64_to_bytes("TriggerInqDelay", 42, 4, Sign::Signed).unwrap(),
1330 ),
1331 ]);
1332
1333 assert_eq!(
1335 nodemap
1336 .get_integer("TriggerInqDelay", &io)
1337 .expect("read indexed register"),
1338 42
1339 );
1340
1341 nodemap
1343 .set_integer("TriggerSelectorIdx", 0, &io)
1344 .expect("select index 0");
1345 assert_eq!(
1346 nodemap
1347 .get_integer("TriggerInqDelay", &io)
1348 .expect("read indexed register"),
1349 1
1350 );
1351 }
1352
1353 #[test]
1354 fn enum_literal_entry_read() {
1355 let nodemap = build_enum_pvalue_nodemap();
1356 let io = MockIo::with_registers(&[
1357 (0x4000, i64_to_bytes("Mode", 10, 4, Sign::Signed).unwrap()),
1358 (
1359 0x4100,
1360 i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1361 ),
1362 ]);
1363
1364 let value = nodemap.get_enum("Mode", &io).expect("read mode");
1365 assert_eq!(value, "Fixed10");
1366 assert_eq!(
1367 io.read_count(0x4100),
1368 1,
1369 "provider should be read once for mapping"
1370 );
1371 }
1372
1373 #[test]
1374 fn enum_provider_entry_read() {
1375 let nodemap = build_enum_pvalue_nodemap();
1376 let io = MockIo::with_registers(&[
1377 (0x4000, i64_to_bytes("Mode", 42, 4, Sign::Signed).unwrap()),
1378 (
1379 0x4100,
1380 i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1381 ),
1382 ]);
1383
1384 let value = nodemap.get_enum("Mode", &io).expect("read dynamic mode");
1385 assert_eq!(value, "DynFromReg");
1386 assert_eq!(io.read_count(0x4100), 1);
1387 }
1388
1389 #[test]
1390 fn enum_set_uses_provider_value() {
1391 let mut nodemap = build_enum_pvalue_nodemap();
1392 let io = MockIo::with_registers(&[
1393 (0x4000, i64_to_bytes("Mode", 0, 4, Sign::Signed).unwrap()),
1394 (
1395 0x4100,
1396 i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1397 ),
1398 ]);
1399
1400 nodemap
1401 .set_enum("Mode", "DynFromReg", &io)
1402 .expect("write enum");
1403 let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap(), Sign::Signed).unwrap();
1404 assert_eq!(raw, 42);
1405 assert_eq!(io.read_count(0x4100), 1);
1406 }
1407
1408 #[test]
1409 fn enum_provider_update_invalidates_mapping() {
1410 let mut nodemap = build_enum_pvalue_nodemap();
1411 let io = MockIo::with_registers(&[
1412 (0x4000, i64_to_bytes("Mode", 42, 4, Sign::Signed).unwrap()),
1413 (
1414 0x4100,
1415 i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1416 ),
1417 ]);
1418
1419 assert_eq!(nodemap.get_enum("Mode", &io).unwrap(), "DynFromReg");
1420 assert_eq!(io.read_count(0x4100), 1);
1421
1422 nodemap
1423 .set_integer("RegModeVal", 17, &io)
1424 .expect("update provider");
1425 io.write(0x4000, &i64_to_bytes("Mode", 0, 4, Sign::Signed).unwrap())
1426 .expect("reset mode register");
1427
1428 nodemap
1429 .set_enum("Mode", "DynFromReg", &io)
1430 .expect("write enum after provider change");
1431 let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap(), Sign::Signed).unwrap();
1432 assert_eq!(raw, 17);
1433 }
1434
1435 #[test]
1436 fn enum_unknown_value_error() {
1437 let nodemap = build_enum_pvalue_nodemap();
1438 let io = MockIo::with_registers(&[
1439 (0x4000, i64_to_bytes("Mode", 99, 4, Sign::Signed).unwrap()),
1440 (
1441 0x4100,
1442 i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1443 ),
1444 ]);
1445
1446 let err = nodemap.get_enum("Mode", &io).unwrap_err();
1447 match err {
1448 GenApiError::EnumValueUnknown { node, value } => {
1449 assert_eq!(node, "Mode");
1450 assert_eq!(value, 99);
1451 }
1452 other => panic!("unexpected error: {other:?}"),
1453 }
1454 }
1455
1456 #[test]
1457 fn enum_entries_are_sorted() {
1458 let nodemap = build_enum_pvalue_nodemap();
1459 let entries = nodemap.enum_entries("Mode").expect("entries");
1460 assert_eq!(
1461 entries,
1462 vec!["DynFromReg".to_string(), "Fixed10".to_string()]
1463 );
1464 }
1465
1466 #[test]
1467 fn bitfield_le_integer_roundtrip() {
1468 let mut nodemap = build_bitfield_nodemap();
1469 let io = MockIo::with_registers(&[(0x5000, vec![0xAA, 0xBB, 0xCC, 0xDD])]);
1470
1471 let value = nodemap
1472 .get_integer("LeByte", &io)
1473 .expect("read little-endian field");
1474 assert_eq!(value, 0xBB);
1475
1476 nodemap
1477 .set_integer("LeByte", 0x55, &io)
1478 .expect("write little-endian field");
1479 let data = io.read(0x5000, 4).expect("read back register");
1480 assert_eq!(data, vec![0xAA, 0x55, 0xCC, 0xDD]);
1481 }
1482
1483 #[test]
1484 fn bitfield_be_integer_roundtrip() {
1485 let mut nodemap = build_bitfield_nodemap();
1486 let io = MockIo::with_registers(&[(0x5004, vec![0b1010_0000, 0b0000_0000])]);
1487
1488 let value = nodemap
1489 .get_integer("BeBits", &io)
1490 .expect("read big-endian bits");
1491 assert_eq!(value, 0b101);
1492
1493 nodemap
1494 .set_integer("BeBits", 0b010, &io)
1495 .expect("write big-endian bits");
1496 let data = io.read(0x5004, 2).expect("read back register");
1497 assert_eq!(data, vec![0b0100_0000, 0b0000_0000]);
1498 }
1499
1500 #[test]
1501 fn bitfield_boolean_toggle() {
1502 let mut nodemap = build_bitfield_nodemap();
1503 let io = MockIo::with_registers(&[(0x5006, vec![0x00, 0x20, 0x00, 0x00])]);
1504
1505 assert!(nodemap.get_bool("PackedFlag", &io).expect("read flag"));
1506
1507 nodemap
1508 .set_bool("PackedFlag", false, &io)
1509 .expect("clear flag");
1510 let data = io.read(0x5006, 4).expect("read cleared");
1511 assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]);
1512
1513 nodemap.set_bool("PackedFlag", true, &io).expect("set flag");
1514 let data = io.read(0x5006, 4).expect("read set");
1515 assert_eq!(data, vec![0x00, 0x20, 0x00, 0x00]);
1516 }
1517
1518 #[test]
1519 fn bitfield_value_too_wide() {
1520 let mut nodemap = build_bitfield_nodemap();
1521 let io = MockIo::with_registers(&[(0x5004, vec![0x00, 0x00])]);
1522
1523 let err = nodemap
1524 .set_integer("BeBits", 8, &io)
1525 .expect_err("value too wide");
1526 match err {
1527 GenApiError::ValueTooWide {
1528 name, bit_length, ..
1529 } => {
1530 assert_eq!(name, "BeBits");
1531 assert_eq!(bit_length, 3);
1532 }
1533 other => panic!("unexpected error: {other:?}"),
1534 }
1535 }
1536 #[test]
1537 fn swissknife_evaluates_and_invalidates() {
1538 let mut nodemap = build_swissknife_nodemap();
1539 let io = MockIo::with_registers(&[
1540 (
1541 0x3000,
1542 i64_to_bytes("GainRaw", 100, 4, Sign::Signed).unwrap(),
1543 ),
1544 (0x3008, i64_to_bytes("Offset", 3, 4, Sign::Signed).unwrap()),
1545 (0x3010, i64_to_bytes("B", 1, 4, Sign::Signed).unwrap()),
1546 ]);
1547
1548 let value = nodemap
1549 .get_float("ComputedGain", &io)
1550 .expect("compute gain");
1551 assert!((value - 53.0).abs() < 1e-6);
1552
1553 nodemap
1554 .set_integer("GainRaw", 120, &io)
1555 .expect("update raw gain");
1556 let updated = nodemap
1557 .get_float("ComputedGain", &io)
1558 .expect("recompute gain");
1559 assert!((updated - 63.0).abs() < 1e-6);
1560 }
1561
1562 #[test]
1563 fn swissknife_integer_rounding_and_unary() {
1564 let mut nodemap = build_swissknife_nodemap();
1565 let io = MockIo::with_registers(&[
1566 (0x3000, i64_to_bytes("GainRaw", 5, 4, Sign::Signed).unwrap()),
1567 (0x3008, i64_to_bytes("Offset", 0, 4, Sign::Signed).unwrap()),
1568 (0x3010, i64_to_bytes("B", 1, 4, Sign::Signed).unwrap()),
1569 ]);
1570
1571 let divided = nodemap
1574 .get_integer("DivideInt", &io)
1575 .expect("integer division");
1576 assert_eq!(divided, 1);
1577
1578 nodemap
1579 .set_integer("GainRaw", 3, &io)
1580 .expect("update gain raw");
1581 let unary = nodemap.get_integer("Unary", &io).expect("unary expression");
1582 assert_eq!(unary, 7);
1583 }
1584
1585 #[test]
1586 fn swissknife_unknown_variable_error() {
1587 const XML: &str = r#"
1588 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1589 <Integer Name="A">
1590 <Address>0x2000</Address>
1591 <Length>4</Length>
1592 <AccessMode>RW</AccessMode>
1593 <Min>0</Min>
1594 <Max>100</Max>
1595 </Integer>
1596 <SwissKnife Name="Bad">
1597 <Expression>A + Missing</Expression>
1598 <pVariable Name="A">A</pVariable>
1599 </SwissKnife>
1600 </RegisterDescription>
1601 "#;
1602
1603 let model = viva_genapi_xml::parse(XML).expect("parse invalid swissknife");
1604 let nodemap = NodeMap::try_from_xml(model).expect("model builds despite the bad node");
1605
1606 let skipped = nodemap.skipped();
1609 assert_eq!(skipped.len(), 1, "expected exactly one dropped node");
1610 assert_eq!(skipped[0].name.as_deref(), Some("Bad"));
1611 assert!(
1612 skipped[0].error.contains("Missing"),
1613 "the record should name the unresolved variable: {}",
1614 skipped[0].error
1615 );
1616 assert!(nodemap.node("Bad").is_none());
1617
1618 assert!(nodemap.node("A").is_some());
1620 }
1621
1622 #[test]
1624 fn unparsable_formula_is_isolated() {
1625 const XML: &str = r#"
1626 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1627 <Integer Name="Width">
1628 <Address>0x2000</Address>
1629 <Length>4</Length>
1630 <AccessMode>RW</AccessMode>
1631 <Min>0</Min>
1632 <Max>4096</Max>
1633 </Integer>
1634 <IntSwissKnife Name="Broken">
1635 <Formula>Width +</Formula>
1636 <pVariable Name="Width">Width</pVariable>
1637 </IntSwissKnife>
1638 </RegisterDescription>
1639 "#;
1640
1641 let model = viva_genapi_xml::parse(XML).expect("parse model");
1642 let nodemap = NodeMap::try_from_xml(model).expect("model builds despite the bad formula");
1643 assert_eq!(nodemap.skipped().len(), 1);
1644 assert_eq!(nodemap.skipped()[0].name.as_deref(), Some("Broken"));
1645 assert!(nodemap.node("Width").is_some());
1646 }
1647
1648 #[test]
1649 fn swissknife_division_by_zero() {
1650 let nodemap = build_swissknife_nodemap();
1651 let io = MockIo::with_registers(&[
1652 (
1653 0x3000,
1654 i64_to_bytes("GainRaw", 10, 4, Sign::Signed).unwrap(),
1655 ),
1656 (0x3008, i64_to_bytes("Offset", 0, 4, Sign::Signed).unwrap()),
1657 (0x3010, i64_to_bytes("B", 0, 4, Sign::Signed).unwrap()),
1658 ]);
1659
1660 let err = nodemap
1661 .get_float("DivideByZero", &io)
1662 .expect_err("division by zero");
1663 match err {
1664 GenApiError::ExprEval { name, msg } => {
1665 assert_eq!(name, "DivideByZero");
1666 assert_eq!(msg, "division by zero");
1667 }
1668 other => panic!("unexpected error: {other:?}"),
1669 }
1670 }
1671
1672 const VISIBILITY_FIXTURE: &str = r#"
1677 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1678 <Integer Name="BeginnerNode">
1679 <Address>0x6000</Address>
1680 <Length>4</Length>
1681 <AccessMode>RW</AccessMode>
1682 <Visibility>Beginner</Visibility>
1683 <Min>0</Min>
1684 <Max>100</Max>
1685 </Integer>
1686 <Integer Name="ExpertNode">
1687 <Address>0x6010</Address>
1688 <Length>4</Length>
1689 <AccessMode>RW</AccessMode>
1690 <Visibility>Expert</Visibility>
1691 <Min>0</Min>
1692 <Max>100</Max>
1693 </Integer>
1694 <Integer Name="GuruNode">
1695 <Address>0x6020</Address>
1696 <Length>4</Length>
1697 <AccessMode>RW</AccessMode>
1698 <Visibility>Guru</Visibility>
1699 <Min>0</Min>
1700 <Max>100</Max>
1701 </Integer>
1702 <Integer Name="InvisibleNode">
1703 <Address>0x6030</Address>
1704 <Length>4</Length>
1705 <AccessMode>RW</AccessMode>
1706 <Visibility>Invisible</Visibility>
1707 <Min>0</Min>
1708 <Max>100</Max>
1709 </Integer>
1710 </RegisterDescription>
1711 "#;
1712
1713 #[test]
1714 fn nodes_at_visibility_beginner_returns_only_beginner() {
1715 let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
1716 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1717
1718 let visible = nodemap.nodes_at_visibility(Visibility::Beginner);
1719 assert!(
1720 visible.contains(&"BeginnerNode"),
1721 "Beginner node must be visible at Beginner level"
1722 );
1723 assert!(
1724 !visible.contains(&"ExpertNode"),
1725 "Expert node must NOT be visible at Beginner level"
1726 );
1727 assert!(
1728 !visible.contains(&"GuruNode"),
1729 "Guru node must NOT be visible at Beginner level"
1730 );
1731 assert!(
1732 !visible.contains(&"InvisibleNode"),
1733 "Invisible node must NOT be visible at Beginner level"
1734 );
1735 }
1736
1737 #[test]
1738 fn nodes_at_visibility_guru_includes_beginner_and_expert_but_not_invisible() {
1739 let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
1740 let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1741
1742 let visible = nodemap.nodes_at_visibility(Visibility::Guru);
1743 assert!(
1744 visible.contains(&"BeginnerNode"),
1745 "Beginner node must be visible at Guru level"
1746 );
1747 assert!(
1748 visible.contains(&"ExpertNode"),
1749 "Expert node must be visible at Guru level"
1750 );
1751 assert!(
1752 visible.contains(&"GuruNode"),
1753 "Guru node must be visible at Guru level"
1754 );
1755 assert!(
1756 !visible.contains(&"InvisibleNode"),
1757 "Invisible node must NOT be visible at Guru level"
1758 );
1759 }
1760}