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::{AsyncReadExt, AsyncWriteExt};
20use tokio::net::tcp::OwnedWriteHalf;
21use tokio::task::JoinHandle;
22use tokio::time::{Instant, interval};
23
24use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
25use spvirit_codec::spvd_decode::{DecodedValue, PvdDecoder, StructureDesc};
26use spvirit_codec::spvd_encode::{encode_pv_request, encode_pv_request_with_options};
27use spvirit_codec::spvirit_encode::{
28 encode_control_message, encode_get_field_request, encode_monitor_request, encode_put_request,
29};
30
31use crate::client::{ChannelConn, ensure_status_ok, establish_channel, pvget as low_level_pvget};
32use crate::put_encode::encode_put_payload;
33use crate::search::resolve_pv_server;
34use crate::transport::{read_packet, read_until};
35use crate::types::{PvGetError, PvGetResult, PvOptions};
36
37const PVA_VERSION: u8 = 2;
39const QOS_INIT: u8 = 0x08;
41
42static NEXT_IOID: AtomicU32 = AtomicU32::new(1);
43fn alloc_ioid() -> u32 {
44 NEXT_IOID.fetch_add(1, Ordering::Relaxed)
45}
46
47fn build_pv_request(fields: &[&str], is_be: bool) -> Vec<u8> {
53 if fields.is_empty() {
54 vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00]
56 } else {
57 encode_pv_request(fields, is_be)
58 }
59}
60
61#[derive(Debug, Clone, Copy, Default)]
69pub struct MonitorOptions {
70 pub pipeline: Option<u32>,
74}
75
76impl MonitorOptions {
77 pub fn pipelined(queue_size: u32) -> Self {
79 Self {
80 pipeline: if queue_size == 0 {
81 None
82 } else {
83 Some(queue_size)
84 },
85 }
86 }
87}
88
89pub struct PvaClientBuilder {
100 udp_port: u16,
101 tcp_port: u16,
102 timeout: Duration,
103 no_broadcast: bool,
104 name_servers: Vec<SocketAddr>,
105 authnz_user: Option<String>,
106 authnz_host: Option<String>,
107 server_addr: Option<SocketAddr>,
108 search_addr: Option<std::net::IpAddr>,
109 bind_addr: Option<std::net::IpAddr>,
110 debug: bool,
111}
112
113impl PvaClientBuilder {
114 fn new() -> Self {
115 Self {
116 udp_port: 5076,
117 tcp_port: 5075,
118 timeout: Duration::from_secs(5),
119 no_broadcast: false,
120 name_servers: Vec::new(),
121 authnz_user: None,
122 authnz_host: None,
123 server_addr: None,
124 search_addr: None,
125 bind_addr: None,
126 debug: false,
127 }
128 }
129
130 pub fn port(mut self, port: u16) -> Self {
132 self.tcp_port = port;
133 self
134 }
135
136 pub fn udp_port(mut self, port: u16) -> Self {
138 self.udp_port = port;
139 self
140 }
141
142 pub fn timeout(mut self, timeout: Duration) -> Self {
144 self.timeout = timeout;
145 self
146 }
147
148 pub fn no_broadcast(mut self) -> Self {
150 self.no_broadcast = true;
151 self
152 }
153
154 pub fn name_server(mut self, addr: SocketAddr) -> Self {
156 self.name_servers.push(addr);
157 self
158 }
159
160 pub fn authnz_user(mut self, user: impl Into<String>) -> Self {
162 self.authnz_user = Some(user.into());
163 self
164 }
165
166 pub fn authnz_host(mut self, host: impl Into<String>) -> Self {
168 self.authnz_host = Some(host.into());
169 self
170 }
171
172 pub fn server_addr(mut self, addr: SocketAddr) -> Self {
174 self.server_addr = Some(addr);
175 self
176 }
177
178 pub fn search_addr(mut self, addr: std::net::IpAddr) -> Self {
180 self.search_addr = Some(addr);
181 self
182 }
183
184 pub fn bind_addr(mut self, addr: std::net::IpAddr) -> Self {
186 self.bind_addr = Some(addr);
187 self
188 }
189
190 pub fn debug(mut self) -> Self {
192 self.debug = true;
193 self
194 }
195
196 pub fn build(self) -> PvaClient {
198 PvaClient {
199 udp_port: self.udp_port,
200 tcp_port: self.tcp_port,
201 timeout: self.timeout,
202 no_broadcast: self.no_broadcast,
203 name_servers: self.name_servers,
204 authnz_user: self.authnz_user,
205 authnz_host: self.authnz_host,
206 server_addr: self.server_addr,
207 search_addr: self.search_addr,
208 bind_addr: self.bind_addr,
209 debug: self.debug,
210 }
211 }
212}
213
214#[derive(Clone, Debug)]
226pub struct PvaClient {
227 udp_port: u16,
228 tcp_port: u16,
229 timeout: Duration,
230 no_broadcast: bool,
231 name_servers: Vec<SocketAddr>,
232 authnz_user: Option<String>,
233 authnz_host: Option<String>,
234 server_addr: Option<SocketAddr>,
235 search_addr: Option<std::net::IpAddr>,
236 bind_addr: Option<std::net::IpAddr>,
237 debug: bool,
238}
239
240impl PvaClient {
241 pub fn builder() -> PvaClientBuilder {
243 PvaClientBuilder::new()
244 }
245
246 fn opts(&self, pv_name: &str) -> PvOptions {
248 let mut o = PvOptions::new(pv_name.to_string());
249 o.udp_port = self.udp_port;
250 o.tcp_port = self.tcp_port;
251 o.timeout = self.timeout;
252 o.no_broadcast = self.no_broadcast;
253 o.name_servers.clone_from(&self.name_servers);
254 o.authnz_user.clone_from(&self.authnz_user);
255 o.authnz_host.clone_from(&self.authnz_host);
256 o.server_addr = self.server_addr;
257 o.search_addr = self.search_addr;
258 o.bind_addr = self.bind_addr;
259 o.debug = self.debug;
260 o
261 }
262
263 async fn open_channel(&self, pv_name: &str) -> Result<ChannelConn, PvGetError> {
265 let opts = self.opts(pv_name);
266 let target = resolve_pv_server(&opts).await?;
267 establish_channel(target, &opts).await
268 }
269
270 pub async fn pvget(&self, pv_name: &str) -> Result<PvGetResult, PvGetError> {
274 let opts = self.opts(pv_name);
275 low_level_pvget(&opts).await
276 }
277
278 pub async fn pvget_fields(
280 &self,
281 pv_name: &str,
282 fields: &[&str],
283 ) -> Result<PvGetResult, PvGetError> {
284 let opts = self.opts(pv_name);
285 crate::client::pvget_fields(&opts, fields).await
286 }
287
288 pub async fn pvput(&self, pv_name: &str, value: impl Into<Value>) -> Result<(), PvGetError> {
299 self.pvput_fields(pv_name, value, &["value"]).await
302 }
303
304 pub async fn pvput_fields(
309 &self,
310 pv_name: &str,
311 value: impl Into<Value>,
312 fields: &[&str],
313 ) -> Result<(), PvGetError> {
314 let json_val = value.into();
315 let ChannelConn {
316 mut stream,
317 sid,
318 version: _,
319 is_be,
320 ..
321 } = self.open_channel(pv_name).await?;
322
323 let ioid = alloc_ioid();
324
325 let pv_request = build_pv_request(fields, is_be);
327 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
328 stream.write_all(&init).await?;
329
330 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
332 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
333 })
334 .await?;
335
336 let desc = decode_init_introspection(&init_bytes, "PUT")?;
337
338 let payload = encode_put_payload(&desc, &json_val, is_be)
340 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
341 let req = encode_put_request(sid, ioid, 0x00, &payload, PVA_VERSION, is_be);
342 stream.write_all(&req).await?;
343
344 let resp_bytes = read_until(
346 &mut stream,
347 self.timeout,
348 |cmd| matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.subcmd == 0x00),
349 )
350 .await?;
351 ensure_status_ok(&resp_bytes, is_be, "PUT")?;
352
353 Ok(())
354 }
355
356 pub async fn open_put_channel(&self, pv_name: &str) -> Result<PvaChannel, PvGetError> {
364 self.open_put_channel_fields(pv_name, &["value"]).await
365 }
366
367 pub async fn open_put_channel_fields(
371 &self,
372 pv_name: &str,
373 fields: &[&str],
374 ) -> Result<PvaChannel, PvGetError> {
375 let ChannelConn {
376 mut stream,
377 sid,
378 version,
379 is_be,
380 ..
381 } = self.open_channel(pv_name).await?;
382
383 let ioid = alloc_ioid();
384
385 let pv_request = build_pv_request(fields, is_be);
387 let init = encode_put_request(sid, ioid, QOS_INIT, &pv_request, PVA_VERSION, is_be);
388 stream.write_all(&init).await?;
389
390 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
391 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
392 })
393 .await?;
394
395 let desc = decode_init_introspection(&init_bytes, "PUT")?;
396
397 let (mut reader, writer) = stream.into_split();
399 let reader_is_be = is_be;
400 let reader_handle = tokio::spawn(async move {
401 loop {
402 let mut header = [0u8; 8];
403 if reader.read_exact(&mut header).await.is_err() {
404 break;
405 }
406 let hdr = spvirit_codec::epics_decode::PvaHeader::new(&header);
407 let len = if hdr.flags.is_control {
408 0usize
409 } else {
410 hdr.payload_length as usize
411 };
412 let mut payload = vec![0u8; len];
413 if len > 0 && reader.read_exact(&mut payload).await.is_err() {
414 break;
415 }
416 if hdr.command == 11 && !hdr.flags.is_control && len >= 5 {
417 if let Some(st) =
418 spvirit_codec::epics_decode::decode_status(&payload[5..], reader_is_be).0
419 {
420 if st.code != 0 {
421 let msg = st.message.unwrap_or_else(|| format!("code={}", st.code));
422 eprintln!("PvaChannel put error: {msg}");
423 }
424 }
425 }
426 }
427 });
428
429 Ok(PvaChannel {
430 writer,
431 sid,
432 ioid,
433 version,
434 is_be,
435 put_desc: desc,
436 echo_token: 1,
437 last_echo: Instant::now(),
438 _reader_handle: reader_handle,
439 })
440 }
441
442 pub async fn pvmonitor<F>(&self, pv_name: &str, callback: F) -> Result<(), PvGetError>
458 where
459 F: FnMut(&DecodedValue) -> ControlFlow<()>,
460 {
461 self.pvmonitor_fields(pv_name, &[], callback).await
464 }
465
466 pub async fn pvmonitor_fields<F>(
472 &self,
473 pv_name: &str,
474 fields: &[&str],
475 callback: F,
476 ) -> Result<(), PvGetError>
477 where
478 F: FnMut(&DecodedValue) -> ControlFlow<()>,
479 {
480 self.pvmonitor_with_options(pv_name, fields, MonitorOptions::default(), callback)
481 .await
482 }
483
484 pub async fn pvmonitor_with_options<F>(
491 &self,
492 pv_name: &str,
493 fields: &[&str],
494 options: MonitorOptions,
495 mut callback: F,
496 ) -> Result<(), PvGetError>
497 where
498 F: FnMut(&DecodedValue) -> ControlFlow<()>,
499 {
500 let ChannelConn {
501 mut stream,
502 sid,
503 version: _,
504 is_be,
505 ..
506 } = self.open_channel(pv_name).await?;
507
508 let ioid = alloc_ioid();
509 let decoder = PvdDecoder::new(is_be);
510
511 let pipeline_queue = options.pipeline.filter(|&n| n > 0);
512
513 let (pv_request, init_subcmd) = if let Some(qsize) = pipeline_queue {
520 let qs_str = qsize.to_string();
521 let mut body = encode_pv_request_with_options(
522 fields,
523 &[("pipeline", "true"), ("queueSize", qs_str.as_str())],
524 is_be,
525 );
526 let qs_bytes = if is_be {
527 qsize.to_be_bytes()
528 } else {
529 qsize.to_le_bytes()
530 };
531 body.extend_from_slice(&qs_bytes);
532 (body, QOS_INIT | 0x80)
533 } else {
534 (build_pv_request(fields, is_be), QOS_INIT)
535 };
536
537 let init = encode_monitor_request(sid, ioid, init_subcmd, &pv_request, PVA_VERSION, is_be);
538 stream.write_all(&init).await?;
539
540 let init_bytes = read_until(&mut stream, self.timeout, |cmd| {
542 matches!(cmd, PvaPacketCommand::Op(op) if op.command == 13 && (op.subcmd & 0x08) != 0)
543 })
544 .await?;
545
546 let field_desc = decode_init_introspection(&init_bytes, "MONITOR")?;
547
548 let start = encode_monitor_request(sid, ioid, 0x44, &[], PVA_VERSION, is_be);
554 stream.write_all(&start).await?;
555
556 let mut consumed_since_ack: u32 = 0;
561 let ack_threshold: u32 = pipeline_queue.map(|q| (q / 2).max(1)).unwrap_or(0);
562
563 let mut echo_interval = interval(Duration::from_secs(10));
565 let mut echo_token: u32 = 1;
566
567 loop {
568 tokio::select! {
569 _ = echo_interval.tick() => {
570 let msg = encode_control_message(false, is_be, PVA_VERSION, 3, echo_token);
571 echo_token = echo_token.wrapping_add(1);
572 let _ = stream.write_all(&msg).await;
573 }
574 res = read_packet(&mut stream, self.timeout) => {
575 let bytes = match res {
576 Ok(b) => b,
577 Err(PvGetError::Timeout(_)) => continue,
578 Err(e) => return Err(e),
579 };
580 let mut pkt = PvaPacket::new(&bytes);
581 if let Some(PvaPacketCommand::Op(op)) = pkt.decode_payload() {
582 if op.command == 13 && op.ioid == ioid && op.subcmd == 0x00 {
583 let payload = &bytes[8..]; let pos = 5; if let Some((decoded, _)) =
586 decoder.decode_structure_with_bitset(&payload[pos..], &field_desc)
587 {
588 let flow = callback(&decoded);
589
590 if pipeline_queue.is_some() {
591 consumed_since_ack = consumed_since_ack.saturating_add(1);
592 if consumed_since_ack >= ack_threshold {
593 let ack_bytes = if is_be {
594 consumed_since_ack.to_be_bytes()
595 } else {
596 consumed_since_ack.to_le_bytes()
597 };
598 let ack = encode_monitor_request(
599 sid,
600 ioid,
601 0x80,
602 &ack_bytes,
603 PVA_VERSION,
604 is_be,
605 );
606 if stream.write_all(&ack).await.is_err() {
607 return Ok(());
608 }
609 consumed_since_ack = 0;
610 }
611 }
612
613 if flow.is_break() {
614 let destroy = encode_monitor_request(
617 sid,
618 ioid,
619 0x10,
620 &[],
621 PVA_VERSION,
622 is_be,
623 );
624 let _ = stream.write_all(&destroy).await;
625 return Ok(());
626 }
627 }
628 }
629 }
630 }
631 }
632 }
633 }
634
635 pub async fn pvinfo(&self, pv_name: &str) -> Result<StructureDesc, PvGetError> {
639 let result = self.pvinfo_full(pv_name).await?;
640 Ok(result.0)
641 }
642
643 pub async fn pvinfo_full(
645 &self,
646 pv_name: &str,
647 ) -> Result<(StructureDesc, SocketAddr), PvGetError> {
648 let ChannelConn {
649 mut stream,
650 sid,
651 version: _,
652 is_be,
653 server_addr,
654 } = self.open_channel(pv_name).await?;
655
656 let ioid = alloc_ioid();
657 let msg = encode_get_field_request(sid, ioid, None, PVA_VERSION, is_be);
658 stream.write_all(&msg).await?;
659
660 let resp_bytes = read_until(&mut stream, self.timeout, |cmd| {
661 matches!(cmd, PvaPacketCommand::GetField(_))
662 })
663 .await?;
664
665 let mut pkt = PvaPacket::new(&resp_bytes);
666 let cmd = pkt
667 .decode_payload()
668 .ok_or_else(|| PvGetError::Decode("GET_FIELD response decode failed".to_string()))?;
669 match cmd {
670 PvaPacketCommand::GetField(payload) => {
671 if let Some(ref st) = payload.status {
672 if st.is_error() {
673 let msg = st
674 .message
675 .clone()
676 .unwrap_or_else(|| format!("code={}", st.code));
677 return Err(PvGetError::Protocol(format!("GET_FIELD error: {msg}")));
678 }
679 }
680 let desc = payload.introspection.ok_or_else(|| {
681 PvGetError::Decode("missing GET_FIELD introspection".to_string())
682 })?;
683 Ok((desc, server_addr))
684 }
685 _ => Err(PvGetError::Protocol(
686 "unexpected GET_FIELD response".to_string(),
687 )),
688 }
689 }
690
691 pub async fn pvlist(&self, server_addr: SocketAddr) -> Result<Vec<String>, PvGetError> {
695 let opts = self.opts("__pvlist");
696 crate::pvlist::pvlist(&opts, server_addr).await
697 }
698
699 pub async fn pvlist_with_fallback(
703 &self,
704 server_addr: SocketAddr,
705 ) -> Result<(Vec<String>, crate::pvlist::PvListSource), PvGetError> {
706 let opts = self.opts("__pvlist");
707 crate::pvlist::pvlist_with_fallback(&opts, server_addr).await
708 }
709}
710
711pub struct PvaChannel {
729 writer: OwnedWriteHalf,
730 sid: u32,
731 ioid: u32,
732 version: u8,
733 is_be: bool,
734 put_desc: StructureDesc,
735 echo_token: u32,
736 last_echo: Instant,
737 _reader_handle: JoinHandle<()>,
738}
739
740impl PvaChannel {
741 pub async fn put(&mut self, value: impl Into<Value>) -> Result<(), PvGetError> {
746 if self.last_echo.elapsed() >= Duration::from_secs(10) {
748 let msg = encode_control_message(false, self.is_be, self.version, 3, self.echo_token);
749 self.echo_token = self.echo_token.wrapping_add(1);
750 let _ = self.writer.write_all(&msg).await;
751 self.last_echo = Instant::now();
752 }
753
754 let json_val = value.into();
755 let payload = encode_put_payload(&self.put_desc, &json_val, self.is_be)
756 .map_err(|e| PvGetError::Protocol(format!("put encode: {e}")))?;
757 let req = encode_put_request(
758 self.sid,
759 self.ioid,
760 0x00,
761 &payload,
762 self.version,
763 self.is_be,
764 );
765 self.writer.write_all(&req).await?;
766 Ok(())
767 }
768
769 pub fn introspection(&self) -> &StructureDesc {
771 &self.put_desc
772 }
773}
774
775impl Drop for PvaChannel {
776 fn drop(&mut self) {
777 self._reader_handle.abort();
778 }
779}
780
781pub async fn pvput(opts: &PvOptions, value: impl Into<Value>) -> Result<(), PvGetError> {
791 let client = client_from_opts(opts);
792 client.pvput(&opts.pv_name, value).await
793}
794
795pub async fn pvmonitor<F>(opts: &PvOptions, callback: F) -> Result<(), PvGetError>
801where
802 F: FnMut(&DecodedValue) -> ControlFlow<()>,
803{
804 let client = client_from_opts(opts);
805 client.pvmonitor(&opts.pv_name, callback).await
806}
807
808pub async fn pvmonitor_fields<F>(
810 opts: &PvOptions,
811 fields: &[&str],
812 callback: F,
813) -> Result<(), PvGetError>
814where
815 F: FnMut(&DecodedValue) -> ControlFlow<()>,
816{
817 let client = client_from_opts(opts);
818 client
819 .pvmonitor_fields(&opts.pv_name, fields, callback)
820 .await
821}
822
823pub async fn pvput_fields(
825 opts: &PvOptions,
826 value: impl Into<Value>,
827 fields: &[&str],
828) -> Result<(), PvGetError> {
829 let client = client_from_opts(opts);
830 client.pvput_fields(&opts.pv_name, value, fields).await
831}
832
833pub async fn pvinfo(opts: &PvOptions) -> Result<StructureDesc, PvGetError> {
835 let client = client_from_opts(opts);
836 client.pvinfo(&opts.pv_name).await
837}
838
839pub fn client_from_opts(opts: &PvOptions) -> PvaClient {
843 let mut b = PvaClient::builder()
844 .port(opts.tcp_port)
845 .udp_port(opts.udp_port)
846 .timeout(opts.timeout);
847 if opts.no_broadcast {
848 b = b.no_broadcast();
849 }
850 for ns in &opts.name_servers {
851 b = b.name_server(*ns);
852 }
853 if let Some(ref u) = opts.authnz_user {
854 b = b.authnz_user(u.clone());
855 }
856 if let Some(ref h) = opts.authnz_host {
857 b = b.authnz_host(h.clone());
858 }
859 if let Some(addr) = opts.server_addr {
860 b = b.server_addr(addr);
861 }
862 if let Some(addr) = opts.search_addr {
863 b = b.search_addr(addr);
864 }
865 if let Some(addr) = opts.bind_addr {
866 b = b.bind_addr(addr);
867 }
868 if opts.debug {
869 b = b.debug();
870 }
871 b.build()
872}
873
874pub fn decode_init_introspection(raw: &[u8], label: &str) -> Result<StructureDesc, PvGetError> {
876 let mut pkt = PvaPacket::new(raw);
877 let cmd = pkt
878 .decode_payload()
879 .ok_or_else(|| PvGetError::Decode(format!("{label} init response decode failed")))?;
880
881 match cmd {
882 PvaPacketCommand::Op(op) => {
883 if let Some(ref st) = op.status {
884 if st.is_error() {
885 let msg = st
886 .message
887 .clone()
888 .unwrap_or_else(|| format!("code={}", st.code));
889 return Err(PvGetError::Protocol(format!("{label} init error: {msg}")));
890 }
891 }
892 op.introspection
893 .ok_or_else(|| PvGetError::Decode(format!("missing {label} introspection")))
894 }
895 _ => Err(PvGetError::Protocol(format!(
896 "unexpected {label} init response"
897 ))),
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[test]
906 fn builder_defaults() {
907 let c = PvaClient::builder().build();
908 assert_eq!(c.tcp_port, 5075);
909 assert_eq!(c.udp_port, 5076);
910 assert_eq!(c.timeout, Duration::from_secs(5));
911 assert!(!c.no_broadcast);
912 assert!(c.name_servers.is_empty());
913 }
914
915 #[test]
916 fn builder_overrides() {
917 let c = PvaClient::builder()
918 .port(9075)
919 .udp_port(9076)
920 .timeout(Duration::from_secs(10))
921 .no_broadcast()
922 .name_server("127.0.0.1:5075".parse().unwrap())
923 .authnz_user("testuser")
924 .authnz_host("testhost")
925 .build();
926 assert_eq!(c.tcp_port, 9075);
927 assert_eq!(c.udp_port, 9076);
928 assert_eq!(c.timeout, Duration::from_secs(10));
929 assert!(c.no_broadcast);
930 assert_eq!(c.name_servers.len(), 1);
931 assert_eq!(c.authnz_user.as_deref(), Some("testuser"));
932 assert_eq!(c.authnz_host.as_deref(), Some("testhost"));
933 }
934
935 #[test]
936 fn opts_inherits_client_config() {
937 let c = PvaClient::builder()
938 .port(9075)
939 .udp_port(9076)
940 .timeout(Duration::from_secs(10))
941 .no_broadcast()
942 .build();
943 let o = c.opts("TEST:PV");
944 assert_eq!(o.pv_name, "TEST:PV");
945 assert_eq!(o.tcp_port, 9075);
946 assert_eq!(o.udp_port, 9076);
947 assert_eq!(o.timeout, Duration::from_secs(10));
948 assert!(o.no_broadcast);
949 }
950
951 #[test]
952 fn client_from_opts_roundtrip() {
953 let mut opts = PvOptions::new("X:Y".into());
954 opts.tcp_port = 8075;
955 opts.udp_port = 8076;
956 opts.timeout = Duration::from_secs(3);
957 opts.no_broadcast = true;
958 let c = client_from_opts(&opts);
959 assert_eq!(c.tcp_port, 8075);
960 assert_eq!(c.udp_port, 8076);
961 assert!(c.no_broadcast);
962 }
963
964 #[test]
965 fn pv_get_options_alias_works() {
966 let opts: crate::types::PvGetOptions = PvOptions::new("ALIAS:TEST".into());
968 assert_eq!(opts.pv_name, "ALIAS:TEST");
969 }
970}