1use std::net::SocketAddr;
14use std::ops::ControlFlow;
15use std::sync::atomic::{AtomicU32, Ordering};
16use std::time::Duration;
17
18use serde_json::Value;
19use tokio::io::AsyncWriteExt;
20use tokio::net::tcp::OwnedWriteHalf;
21use tokio::task::JoinHandle;
22use tokio::time::{Instant, interval};
23
24use spvirit_codec::MonitorUpdate;
25use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
26use spvirit_codec::spvd_decode::{PvdDecoder, StructureDesc};
27use spvirit_codec::spvd_encode::{encode_pv_request, encode_pv_request_with_options};
28use spvirit_codec::spvirit_encode::{
29 encode_control_message, encode_get_field_request, encode_monitor_request, encode_put_request,
30};
31
32use crate::client::{ChannelConn, ensure_status_ok, establish_channel, pvget as low_level_pvget};
33use crate::put_encode::encode_put_payload;
34use crate::search::resolve_pv_server;
35use crate::transport::{read_frame, read_packet, read_until};
36use crate::types::{PvGetError, PvGetResult, PvOptions};
37
38const PVA_VERSION: u8 = 2;
40const QOS_INIT: u8 = 0x08;
42
43static NEXT_IOID: AtomicU32 = AtomicU32::new(1);
44fn alloc_ioid() -> u32 {
45 NEXT_IOID.fetch_add(1, Ordering::Relaxed)
46}
47
48fn build_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
54 if fields.is_empty() {
55 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
57 } else {
58 encode_pv_request(fields, is_be)
59 }
60}
61
62#[derive(Debug, Clone, Copy, Default)]
70pub struct MonitorOptions {
71 pub pipeline: Option<u32>,
75}
76
77impl MonitorOptions {
78 pub fn pipelined(queue_size: u32) -> Self {
80 Self {
81 pipeline: if queue_size == 0 {
82 None
83 } else {
84 Some(queue_size)
85 },
86 }
87 }
88}
89
90pub struct PvaClientBuilder {
101 udp_port: u16,
102 tcp_port: u16,
103 timeout: Duration,
104 no_broadcast: bool,
105 name_servers: Vec<SocketAddr>,
106 authnz_user: Option<String>,
107 authnz_host: Option<String>,
108 server_addr: Option<SocketAddr>,
109 search_addr: Option<std::net::IpAddr>,
110 bind_addr: Option<std::net::IpAddr>,
111 debug: bool,
112}
113
114impl PvaClientBuilder {
115 fn new() -> Self {
116 Self {
117 udp_port: 5076,
118 tcp_port: 5075,
119 timeout: Duration::from_secs(5),
120 no_broadcast: false,
121 name_servers: Vec::new(),
122 authnz_user: None,
123 authnz_host: None,
124 server_addr: None,
125 search_addr: None,
126 bind_addr: None,
127 debug: false,
128 }
129 }
130
131 pub fn port(mut self, port: u16) -> Self {
133 self.tcp_port = port;
134 self
135 }
136
137 pub fn udp_port(mut self, port: u16) -> Self {
139 self.udp_port = port;
140 self
141 }
142
143 pub fn timeout(mut self, timeout: Duration) -> Self {
145 self.timeout = timeout;
146 self
147 }
148
149 pub fn no_broadcast(mut self) -> Self {
151 self.no_broadcast = true;
152 self
153 }
154
155 pub fn name_server(mut self, addr: SocketAddr) -> Self {
157 self.name_servers.push(addr);
158 self
159 }
160
161 pub fn authnz_user(mut self, user: impl Into<String>) -> Self {
163 self.authnz_user = Some(user.into());
164 self
165 }
166
167 pub fn authnz_host(mut self, host: impl Into<String>) -> Self {
169 self.authnz_host = Some(host.into());
170 self
171 }
172
173 pub fn server_addr(mut self, addr: SocketAddr) -> Self {
175 self.server_addr = Some(addr);
176 self
177 }
178
179 pub fn search_addr(mut self, addr: std::net::IpAddr) -> Self {
181 self.search_addr = Some(addr);
182 self
183 }
184
185 pub fn bind_addr(mut self, addr: std::net::IpAddr) -> Self {
187 self.bind_addr = Some(addr);
188 self
189 }
190
191 pub fn debug(mut self) -> Self {
193 self.debug = true;
194 self
195 }
196
197 pub fn build(self) -> PvaClient {
199 PvaClient {
200 udp_port: self.udp_port,
201 tcp_port: self.tcp_port,
202 timeout: self.timeout,
203 no_broadcast: self.no_broadcast,
204 name_servers: self.name_servers,
205 authnz_user: self.authnz_user,
206 authnz_host: self.authnz_host,
207 server_addr: self.server_addr,
208 search_addr: self.search_addr,
209 bind_addr: self.bind_addr,
210 debug: self.debug,
211 }
212 }
213}
214
215#[derive(Clone, Debug)]
227pub struct PvaClient {
228 udp_port: u16,
229 tcp_port: u16,
230 timeout: Duration,
231 no_broadcast: bool,
232 name_servers: Vec<SocketAddr>,
233 authnz_user: Option<String>,
234 authnz_host: Option<String>,
235 server_addr: Option<SocketAddr>,
236 search_addr: Option<std::net::IpAddr>,
237 bind_addr: Option<std::net::IpAddr>,
238 debug: bool,
239}
240
241impl PvaClient {
242 pub fn builder() -> PvaClientBuilder {
244 PvaClientBuilder::new()
245 }
246
247 fn opts(&self, pv_name: &str) -> PvOptions {
249 let mut o = PvOptions::new(pv_name.to_string());
250 o.udp_port = self.udp_port;
251 o.tcp_port = self.tcp_port;
252 o.timeout = self.timeout;
253 o.no_broadcast = self.no_broadcast;
254 o.name_servers.clone_from(&self.name_servers);
255 o.authnz_user.clone_from(&self.authnz_user);
256 o.authnz_host.clone_from(&self.authnz_host);
257 o.server_addr = self.server_addr;
258 o.search_addr = self.search_addr;
259 o.bind_addr = self.bind_addr;
260 o.debug = self.debug;
261 o
262 }
263
264 async fn open_channel(&self, pv_name: &str) -> Result<ChannelConn, PvGetError> {
266 let opts = self.opts(pv_name);
267 let target = resolve_pv_server(&opts).await?;
268 establish_channel(target, &opts).await
269 }
270
271 pub async fn pvget(&self, pv_name: &str) -> Result<PvGetResult, PvGetError> {
275 let opts = self.opts(pv_name);
276 low_level_pvget(&opts).await
277 }
278
279 pub async fn pvget_fields(
281 &self,
282 pv_name: &str,
283 fields: &[&str],
284 ) -> Result<PvGetResult, PvGetError> {
285 let opts = self.opts(pv_name);
286 crate::client::pvget_fields(&opts, fields).await
287 }
288
289 pub async fn pvput(&self, pv_name: &str, value: impl Into<Value>) -> Result<(), PvGetError> {
300 self.pvput_fields(pv_name, value, &["value"]).await
303 }
304
305 pub async fn pvput_fields(
310 &self,
311 pv_name: &str,
312 value: impl Into<Value>,
313 fields: &[&str],
314 ) -> Result<(), PvGetError> {
315 let json_val = value.into();
316 let ChannelConn {
317 mut stream,
318 sid,
319 version: _,
320 is_be,
321 mut reassembler,
322 ..
323 } = self.open_channel(pv_name).await?;
324
325 let ioid = alloc_ioid();
326
327 let pv_request = build_pv_request(fields, is_be);
329 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
330 stream.write_all(&init).await?;
331
332 let init_bytes = read_until(&mut stream, self.timeout, &mut reassembler, |cmd| {
334 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
335 })
336 .await?;
337
338 let desc = decode_init_introspection(&init_bytes, "PUT")?;
339
340 let payload = encode_put_payload(&desc, &json_val, is_be)
342 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
343 let req = encode_put_request(sid, ioid, 0x00, &payload, PVA_VERSION, is_be);
344 stream.write_all(&req).await?;
345
346 let resp_bytes = read_until(
348 &mut stream,
349 self.timeout,
350 &mut reassembler,
351 |cmd| matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.subcmd == 0x00),
352 )
353 .await?;
354 ensure_status_ok(&resp_bytes, is_be, "PUT")?;
355
356 Ok(())
357 }
358
359 pub async fn open_put_channel(&self, pv_name: &str) -> Result<PvaChannel, PvGetError> {
367 self.open_put_channel_fields(pv_name, &["value"]).await
368 }
369
370 pub async fn open_put_channel_fields(
374 &self,
375 pv_name: &str,
376 fields: &[&str],
377 ) -> Result<PvaChannel, PvGetError> {
378 let ChannelConn {
379 mut stream,
380 sid,
381 version,
382 is_be,
383 mut reassembler,
384 ..
385 } = self.open_channel(pv_name).await?;
386
387 let ioid = alloc_ioid();
388
389 let pv_request = build_pv_request(fields, is_be);
391 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
392 stream.write_all(&init).await?;
393
394 let init_bytes = read_until(&mut stream, self.timeout, &mut reassembler, |cmd| {
395 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
396 })
397 .await?;
398
399 let desc = decode_init_introspection(&init_bytes, "PUT")?;
400
401 let (mut reader, writer) = stream.into_split();
403 let reader_is_be = is_be;
404 let reader_handle = tokio::spawn(async move {
408 let poll = Duration::from_secs(3600);
411 loop {
412 let msg = match read_frame(&mut reader, poll, &mut reassembler).await {
413 Ok(m) => m,
414 Err(PvGetError::Timeout(_)) => continue,
415 Err(_) => break,
416 };
417 let hdr = spvirit_codec::epics_decode::PvaHeader::new(&msg[..8]);
418 let payload = &msg[8..];
419 if hdr.command == 11 && !hdr.flags.is_control && payload.len() >= 5 {
420 if let Some(st) =
421 spvirit_codec::epics_decode::decode_status(&payload[5..], reader_is_be).0
422 {
423 if st.code != 0 {
424 let msg = st.message.unwrap_or_else(|| format!("code={}", st.code));
425 eprintln!("PvaChannel put error: {msg}");
426 }
427 }
428 }
429 }
430 });
431
432 Ok(PvaChannel {
433 writer,
434 sid,
435 ioid,
436 version,
437 is_be,
438 put_desc: desc,
439 echo_token: 1,
440 last_echo: Instant::now(),
441 _reader_handle: reader_handle,
442 })
443 }
444
445 pub async fn pvmonitor<F>(&self, pv_name: &str, callback: F) -> Result<(), PvGetError>
461 where
462 F: FnMut(&MonitorUpdate) -> ControlFlow<()>,
463 {
464 self.pvmonitor_fields(pv_name, &[], callback).await
467 }
468
469 pub async fn pvmonitor_fields<F>(
475 &self,
476 pv_name: &str,
477 fields: &[&str],
478 callback: F,
479 ) -> Result<(), PvGetError>
480 where
481 F: FnMut(&MonitorUpdate) -> ControlFlow<()>,
482 {
483 self.pvmonitor_with_options(pv_name, fields, MonitorOptions::default(), callback)
484 .await
485 }
486
487 pub async fn pvmonitor_with_options<F>(
494 &self,
495 pv_name: &str,
496 fields: &[&str],
497 options: MonitorOptions,
498 mut callback: F,
499 ) -> Result<(), PvGetError>
500 where
501 F: FnMut(&MonitorUpdate) -> ControlFlow<()>,
502 {
503 let ChannelConn {
504 mut stream,
505 sid,
506 version: _,
507 is_be,
508 mut reassembler,
509 ..
510 } = self.open_channel(pv_name).await?;
511
512 let ioid = alloc_ioid();
513 let decoder = PvdDecoder::new(is_be);
514
515 let pipeline_queue = options.pipeline.filter(|&n| n > 0);
516
517 let (pv_request, init_subcmd) = if let Some(qsize) = pipeline_queue {
524 let qs_str = qsize.to_string();
525 let mut body = encode_pv_request_with_options(
526 fields,
527 &[("pipeline", "true"), ("queueSize", qs_str.as_str())],
528 is_be,
529 );
530 let qs_bytes = if is_be {
531 qsize.to_be_bytes()
532 } else {
533 qsize.to_le_bytes()
534 };
535 body.extend_from_slice(&qs_bytes);
536 (body, QOS_INIT | 0x80)
537 } else {
538 (build_pv_request(fields, is_be), QOS_INIT)
539 };
540
541 let init = encode_monitor_request(sid, ioid, init_subcmd, &pv_request, PVA_VERSION, is_be);
542 stream.write_all(&init).await?;
543
544 let init_bytes = read_until(&mut stream, self.timeout, &mut reassembler, |cmd| {
546 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 13 && (op.subcmd & 0x08) != 0)
547 })
548 .await?;
549
550 let field_desc = decode_init_introspection(&init_bytes, "MONITOR")?;
551
552 let start = encode_monitor_request(sid, ioid, 0x44, &[], PVA_VERSION, is_be);
558 stream.write_all(&start).await?;
559
560 let mut consumed_since_ack: u32 = 0;
565 let ack_threshold: u32 = pipeline_queue.map(|q| (q / 2).max(1)).unwrap_or(0);
566
567 let mut echo_interval = interval(Duration::from_secs(10));
569 let mut echo_token: u32 = 1;
570
571 loop {
572 tokio::select! {
573 _ = echo_interval.tick() => {
574 let msg = encode_control_message(false, is_be, PVA_VERSION, 3, echo_token);
575 echo_token = echo_token.wrapping_add(1);
576 let _ = stream.write_all(&msg).await;
577 }
578 res = read_packet(&mut stream, self.timeout, &mut reassembler) => {
579 let bytes = match res {
580 Ok(b) => b,
581 Err(PvGetError::Timeout(_)) => continue,
582 Err(e) => return Err(e),
583 };
584 let mut pkt = PvaPacket::new(&bytes);
585 if let Some(PvaPacketCommand::Op(op)) = pkt.decode_payload() {
586 if op.command == 13 && op.ioid == ioid && op.subcmd == 0x00 {
587 let payload = &bytes[8..]; let pos = 5; if let Ok(update) =
590 decoder.decode_monitor_update(&payload[pos..], &field_desc)
591 {
592 let flow = callback(&update);
593
594 if pipeline_queue.is_some() {
595 consumed_since_ack = consumed_since_ack.saturating_add(1);
596 if consumed_since_ack >= ack_threshold {
597 let ack_bytes = if is_be {
598 consumed_since_ack.to_be_bytes()
599 } else {
600 consumed_since_ack.to_le_bytes()
601 };
602 let ack = encode_monitor_request(
603 sid,
604 ioid,
605 0x80,
606 &ack_bytes,
607 PVA_VERSION,
608 is_be,
609 );
610 if stream.write_all(&ack).await.is_err() {
611 return Ok(());
612 }
613 consumed_since_ack = 0;
614 }
615 }
616
617 if flow.is_break() {
618 let destroy = encode_monitor_request(
621 sid,
622 ioid,
623 0x10,
624 &[],
625 PVA_VERSION,
626 is_be,
627 );
628 let _ = stream.write_all(&destroy).await;
629 return Ok(());
630 }
631 }
632 }
633 }
634 }
635 }
636 }
637 }
638
639 pub async fn pvinfo(&self, pv_name: &str) -> Result<StructureDesc, PvGetError> {
643 let result = self.pvinfo_full(pv_name).await?;
644 Ok(result.0)
645 }
646
647 pub async fn pvinfo_full(
649 &self,
650 pv_name: &str,
651 ) -> Result<(StructureDesc, SocketAddr), PvGetError> {
652 let ChannelConn {
653 mut stream,
654 sid,
655 version: _,
656 is_be,
657 server_addr,
658 mut reassembler,
659 } = self.open_channel(pv_name).await?;
660
661 let ioid = alloc_ioid();
662 let msg = encode_get_field_request(sid, ioid, None, PVA_VERSION, is_be);
663 stream.write_all(&msg).await?;
664
665 let resp_bytes = read_until(&mut stream, self.timeout, &mut reassembler, |cmd| {
666 matches!(cmd, PvaPacketCommand::GetField(_))
667 })
668 .await?;
669
670 let mut pkt = PvaPacket::new(&resp_bytes);
671 let cmd = pkt
672 .decode_payload()
673 .ok_or_else(|| PvGetError::Decode("GET_FIELD response decode failed".to_string()))?;
674 match cmd {
675 PvaPacketCommand::GetField(payload) => {
676 if let Some(ref st) = payload.status {
677 if st.is_error() {
678 let msg = st
679 .message
680 .clone()
681 .unwrap_or_else(|| format!("code={}", st.code));
682 return Err(PvGetError::Protocol(format!("GET_FIELD error: {msg}")));
683 }
684 }
685 let desc = payload.introspection.ok_or_else(|| {
686 PvGetError::Decode("missing GET_FIELD introspection".to_string())
687 })?;
688 Ok((desc, server_addr))
689 }
690 _ => Err(PvGetError::Protocol(
691 "unexpected GET_FIELD response".to_string(),
692 )),
693 }
694 }
695
696 pub async fn pvlist(&self, server_addr: SocketAddr) -> Result<Vec<String>, PvGetError> {
700 let opts = self.opts("__pvlist");
701 crate::pvlist::pvlist(&opts, server_addr).await
702 }
703
704 pub async fn pvlist_with_fallback(
708 &self,
709 server_addr: SocketAddr,
710 ) -> Result<(Vec<String>, crate::pvlist::PvListSource), PvGetError> {
711 let opts = self.opts("__pvlist");
712 crate::pvlist::pvlist_with_fallback(&opts, server_addr).await
713 }
714}
715
716pub struct PvaChannel {
734 writer: OwnedWriteHalf,
735 sid: u32,
736 ioid: u32,
737 version: u8,
738 is_be: bool,
739 put_desc: StructureDesc,
740 echo_token: u32,
741 last_echo: Instant,
742 _reader_handle: JoinHandle<()>,
743}
744
745impl PvaChannel {
746 pub async fn put(&mut self, value: impl Into<Value>) -> Result<(), PvGetError> {
751 if self.last_echo.elapsed() >= Duration::from_secs(10) {
753 let msg = encode_control_message(false, self.is_be, self.version, 3, self.echo_token);
754 self.echo_token = self.echo_token.wrapping_add(1);
755 let _ = self.writer.write_all(&msg).await;
756 self.last_echo = Instant::now();
757 }
758
759 let json_val = value.into();
760 let payload = encode_put_payload(&self.put_desc, &json_val, self.is_be)
761 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
762 let req = encode_put_request(
763 self.sid,
764 self.ioid,
765 0x00,
766 &payload,
767 self.version,
768 self.is_be,
769 );
770 self.writer.write_all(&req).await?;
771 Ok(())
772 }
773
774 pub fn introspection(&self) -> &StructureDesc {
776 &self.put_desc
777 }
778}
779
780impl Drop for PvaChannel {
781 fn drop(&mut self) {
782 self._reader_handle.abort();
783 }
784}
785
786pub async fn pvput(opts: &PvOptions, value: impl Into<Value>) -> Result<(), PvGetError> {
796 let client = client_from_opts(opts);
797 client.pvput(&opts.pv_name, value).await
798}
799
800pub async fn pvmonitor<F>(opts: &PvOptions, callback: F) -> Result<(), PvGetError>
806where
807 F: FnMut(&MonitorUpdate) -> ControlFlow<()>,
808{
809 let client = client_from_opts(opts);
810 client.pvmonitor(&opts.pv_name, callback).await
811}
812
813pub async fn pvmonitor_fields<F>(
815 opts: &PvOptions,
816 fields: &[&str],
817 callback: F,
818) -> Result<(), PvGetError>
819where
820 F: FnMut(&MonitorUpdate) -> ControlFlow<()>,
821{
822 let client = client_from_opts(opts);
823 client
824 .pvmonitor_fields(&opts.pv_name, fields, callback)
825 .await
826}
827
828pub async fn pvput_fields(
830 opts: &PvOptions,
831 value: impl Into<Value>,
832 fields: &[&str],
833) -> Result<(), PvGetError> {
834 let client = client_from_opts(opts);
835 client.pvput_fields(&opts.pv_name, value, fields).await
836}
837
838pub async fn pvinfo(opts: &PvOptions) -> Result<StructureDesc, PvGetError> {
840 let client = client_from_opts(opts);
841 client.pvinfo(&opts.pv_name).await
842}
843
844pub fn client_from_opts(opts: &PvOptions) -> PvaClient {
848 let mut b = PvaClient::builder()
849 .port(opts.tcp_port)
850 .udp_port(opts.udp_port)
851 .timeout(opts.timeout);
852 if opts.no_broadcast {
853 b = b.no_broadcast();
854 }
855 for ns in &opts.name_servers {
856 b = b.name_server(*ns);
857 }
858 if let Some(ref u) = opts.authnz_user {
859 b = b.authnz_user(u.clone());
860 }
861 if let Some(ref h) = opts.authnz_host {
862 b = b.authnz_host(h.clone());
863 }
864 if let Some(addr) = opts.server_addr {
865 b = b.server_addr(addr);
866 }
867 if let Some(addr) = opts.search_addr {
868 b = b.search_addr(addr);
869 }
870 if let Some(addr) = opts.bind_addr {
871 b = b.bind_addr(addr);
872 }
873 if opts.debug {
874 b = b.debug();
875 }
876 b.build()
877}
878
879pub fn decode_init_introspection(raw: &[u8], label: &str) -> Result<StructureDesc, PvGetError> {
881 let mut pkt = PvaPacket::new(raw);
882 let cmd = pkt
883 .decode_payload()
884 .ok_or_else(|| PvGetError::Decode(format!("{label} init response decode failed")))?;
885
886 match cmd {
887 PvaPacketCommand::Op(op) => {
888 if let Some(ref st) = op.status {
889 if st.is_error() {
890 let msg = st
891 .message
892 .clone()
893 .unwrap_or_else(|| format!("code={}", st.code));
894 return Err(PvGetError::Protocol(format!("{label} init error: {msg}")));
895 }
896 }
897 op.introspection
898 .ok_or_else(|| PvGetError::Decode(format!("missing {label} introspection")))
899 }
900 _ => Err(PvGetError::Protocol(format!(
901 "unexpected {label} init response"
902 ))),
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[test]
911 fn builder_defaults() {
912 let c = PvaClient::builder().build();
913 assert_eq!(c.tcp_port, 5075);
914 assert_eq!(c.udp_port, 5076);
915 assert_eq!(c.timeout, Duration::from_secs(5));
916 assert!(!c.no_broadcast);
917 assert!(c.name_servers.is_empty());
918 }
919
920 #[test]
921 fn builder_overrides() {
922 let c = PvaClient::builder()
923 .port(9075)
924 .udp_port(9076)
925 .timeout(Duration::from_secs(10))
926 .no_broadcast()
927 .name_server("127.0.0.1:5075".parse().unwrap())
928 .authnz_user("testuser")
929 .authnz_host("testhost")
930 .build();
931 assert_eq!(c.tcp_port, 9075);
932 assert_eq!(c.udp_port, 9076);
933 assert_eq!(c.timeout, Duration::from_secs(10));
934 assert!(c.no_broadcast);
935 assert_eq!(c.name_servers.len(), 1);
936 assert_eq!(c.authnz_user.as_deref(), Some("testuser"));
937 assert_eq!(c.authnz_host.as_deref(), Some("testhost"));
938 }
939
940 #[test]
941 fn opts_inherits_client_config() {
942 let c = PvaClient::builder()
943 .port(9075)
944 .udp_port(9076)
945 .timeout(Duration::from_secs(10))
946 .no_broadcast()
947 .build();
948 let o = c.opts("TEST:PV");
949 assert_eq!(o.pv_name, "TEST:PV");
950 assert_eq!(o.tcp_port, 9075);
951 assert_eq!(o.udp_port, 9076);
952 assert_eq!(o.timeout, Duration::from_secs(10));
953 assert!(o.no_broadcast);
954 }
955
956 #[test]
957 fn client_from_opts_roundtrip() {
958 let mut opts = PvOptions::new("X:Y".into());
959 opts.tcp_port = 8075;
960 opts.udp_port = 8076;
961 opts.timeout = Duration::from_secs(3);
962 opts.no_broadcast = true;
963 let c = client_from_opts(&opts);
964 assert_eq!(c.tcp_port, 8075);
965 assert_eq!(c.udp_port, 8076);
966 assert!(c.no_broadcast);
967 }
968
969 #[test]
970 fn pv_get_options_alias_works() {
971 let opts: crate::types::PvGetOptions = PvOptions::new("ALIAS:TEST".into());
973 assert_eq!(opts.pv_name, "ALIAS:TEST");
974 }
975}