1use crate::symmetric::{CollectiveError, Rank, SymmetricBuffer, SymmetricTransport};
45use crate::transport::{Transport, default_barrier};
46use std::collections::{HashMap, VecDeque};
47use std::io::{self, Read, Write};
48use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream};
49use std::sync::atomic::{AtomicBool, Ordering};
50use std::sync::{Arc, Condvar, Mutex, RwLock};
51use std::thread::{self, JoinHandle};
52use std::time::{Duration, Instant};
53
54const HELLO: u8 = 1; const SEND: u8 = 2; const PUT: u8 = 3; const GETREQ: u8 = 4; const GETRESP: u8 = 5; const HEADER_LEN: usize = 13;
62
63fn write_frame<W: Write>(w: &mut W, kind: u8, a: u32, b: u32, payload: &[u8]) -> io::Result<()> {
64 let mut hdr = [0u8; HEADER_LEN];
65 hdr[0] = kind;
66 hdr[1..5].copy_from_slice(&a.to_le_bytes());
67 hdr[5..9].copy_from_slice(&b.to_le_bytes());
68 hdr[9..13].copy_from_slice(&(payload.len() as u32).to_le_bytes());
69 const COALESCE_MAX: usize = 1 << 16;
75 if payload.len() <= COALESCE_MAX {
76 let mut buf = Vec::with_capacity(HEADER_LEN + payload.len());
77 buf.extend_from_slice(&hdr);
78 buf.extend_from_slice(payload);
79 w.write_all(&buf)?;
80 } else {
81 w.write_all(&hdr)?;
82 w.write_all(payload)?;
83 }
84 w.flush()
85}
86
87fn read_frame<R: Read>(r: &mut R) -> io::Result<(u8, u32, u32, Vec<u8>)> {
88 let mut hdr = [0u8; HEADER_LEN];
89 r.read_exact(&mut hdr)?;
90 let kind = hdr[0];
91 let a = u32::from_le_bytes(hdr[1..5].try_into().unwrap());
92 let b = u32::from_le_bytes(hdr[5..9].try_into().unwrap());
93 let len = u32::from_le_bytes(hdr[9..13].try_into().unwrap()) as usize;
94 let mut payload = vec![0u8; len];
95 if len > 0 {
96 r.read_exact(&mut payload)?;
97 }
98 Ok((kind, a, b, payload))
99}
100
101fn connect_retry(addr: SocketAddr) -> io::Result<TcpStream> {
102 let mut last: Option<io::Error> = None;
103 for _ in 0..400 {
104 match TcpStream::connect(addr) {
105 Ok(s) => return Ok(s),
106 Err(e) => {
107 last = Some(e);
108 thread::sleep(Duration::from_millis(25));
109 }
110 }
111 }
112 Err(last.unwrap_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "connect retry exhausted")))
113}
114
115struct NetInner {
116 rank: u32,
117 world: u32,
118 heap_size: usize,
119 writers: Vec<Mutex<Option<TcpStream>>>,
122 inbox: Mutex<HashMap<(u32, u32), VecDeque<Vec<u8>>>>,
124 inbox_cv: Condvar,
125 heap: RwLock<Vec<u8>>,
127 getresp: Mutex<HashMap<u32, VecDeque<Vec<u8>>>>,
129 getresp_cv: Condvar,
130 shutdown: AtomicBool,
131}
132
133impl NetInner {
134 fn send_frame(
135 &self,
136 peer: u32,
137 kind: u8,
138 a: u32,
139 b: u32,
140 payload: &[u8],
141 ) -> Result<(), CollectiveError> {
142 let mut guard = self.writers[peer as usize].lock().unwrap();
143 match guard.as_mut() {
144 Some(s) => {
145 write_frame(s, kind, a, b, payload).map_err(|e| CollectiveError::TransportError {
146 reason: format!("send to rank {peer}: {e}"),
147 })
148 }
149 None => Err(CollectiveError::TransportError {
150 reason: format!("no connection to rank {peer}"),
151 }),
152 }
153 }
154
155 fn recv_inbox(&self, from: u32, tag: u32) -> Vec<u8> {
156 let mut guard = self.inbox.lock().unwrap();
157 loop {
158 if let Some(q) = guard.get_mut(&(from, tag))
159 && let Some(v) = q.pop_front()
160 {
161 return v;
162 }
163 guard = self.inbox_cv.wait(guard).unwrap();
164 }
165 }
166
167 fn recv_inbox_timeout(&self, from: u32, tag: u32, timeout: Duration) -> Option<Vec<u8>> {
171 let deadline = Instant::now() + timeout;
172 let mut guard = self.inbox.lock().unwrap();
173 loop {
174 if let Some(q) = guard.get_mut(&(from, tag))
175 && let Some(v) = q.pop_front()
176 {
177 return Some(v);
178 }
179 let now = Instant::now();
180 if now >= deadline {
181 return None;
182 }
183 let (g, res) = self.inbox_cv.wait_timeout(guard, deadline - now).unwrap();
184 guard = g;
185 if res.timed_out()
186 && let Some(q) = guard.get_mut(&(from, tag))
187 && let Some(v) = q.pop_front()
188 {
189 return Some(v);
190 }
191 }
192 }
193
194 fn wait_getresp(&self, peer: u32) -> Vec<u8> {
195 let mut guard = self.getresp.lock().unwrap();
196 loop {
197 if let Some(q) = guard.get_mut(&peer)
198 && let Some(v) = q.pop_front()
199 {
200 return v;
201 }
202 guard = self.getresp_cv.wait(guard).unwrap();
203 }
204 }
205
206 fn check_buf(&self, buf: SymmetricBuffer) -> Result<(), CollectiveError> {
207 if buf.rank.0 >= self.world {
208 return Err(CollectiveError::UnknownRank {
209 rank: buf.rank,
210 num_ranks: self.world,
211 });
212 }
213 if buf.offset + buf.len > self.heap_size {
214 return Err(CollectiveError::OutOfBounds {
215 rank: buf.rank,
216 offset: buf.offset,
217 len: buf.len,
218 heap_size: self.heap_size,
219 });
220 }
221 Ok(())
222 }
223
224 fn dispatch(&self, kind: u8, a: u32, b: u32, payload: Vec<u8>) {
225 match kind {
226 SEND => {
227 self.inbox
228 .lock()
229 .unwrap()
230 .entry((a, b))
231 .or_default()
232 .push_back(payload);
233 self.inbox_cv.notify_all();
234 }
235 PUT => {
236 let off = b as usize;
237 if off + payload.len() <= self.heap_size {
238 let mut h = self.heap.write().unwrap();
239 h[off..off + payload.len()].copy_from_slice(&payload);
240 }
241 }
242 GETREQ if payload.len() >= 8 => {
243 let off = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
244 let rlen = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
245 let data = {
246 let h = self.heap.read().unwrap();
247 if off + rlen <= self.heap_size {
248 h[off..off + rlen].to_vec()
249 } else {
250 Vec::new()
251 }
252 };
253 let _ = self.send_frame(a, GETRESP, self.rank, off as u32, &data);
255 }
256 GETRESP => {
257 self.getresp
258 .lock()
259 .unwrap()
260 .entry(a)
261 .or_default()
262 .push_back(payload);
263 self.getresp_cv.notify_all();
264 }
265 _ => {}
266 }
267 }
268}
269
270fn reader_loop(mut stream: TcpStream, inner: Arc<NetInner>) {
271 while let Ok((kind, a, b, payload)) = read_frame(&mut stream) {
273 inner.dispatch(kind, a, b, payload);
274 if inner.shutdown.load(Ordering::Relaxed) {
275 break;
276 }
277 }
278}
279
280pub struct NetTransport {
284 inner: Arc<NetInner>,
285 readers: Mutex<Vec<JoinHandle<()>>>,
286}
287
288impl NetTransport {
289 pub fn from_listener(
299 rank: u32,
300 world: u32,
301 listener: TcpListener,
302 peers: Vec<SocketAddr>,
303 heap_size: usize,
304 ) -> io::Result<Self> {
305 assert_eq!(
306 peers.len(),
307 world as usize,
308 "peers must have world_size entries"
309 );
310 assert!(rank < world, "rank out of range");
311
312 let inner = Arc::new(NetInner {
313 rank,
314 world,
315 heap_size,
316 writers: (0..world).map(|_| Mutex::new(None)).collect(),
317 inbox: Mutex::new(HashMap::new()),
318 inbox_cv: Condvar::new(),
319 heap: RwLock::new(vec![0u8; heap_size]),
320 getresp: Mutex::new(HashMap::new()),
321 getresp_cv: Condvar::new(),
322 shutdown: AtomicBool::new(false),
323 });
324 let mut readers = Vec::new();
325
326 for p in (rank + 1)..world {
328 let stream = connect_retry(peers[p as usize])?;
329 stream.set_nodelay(true).ok();
330 let mut wr = stream.try_clone()?;
331 write_frame(&mut wr, HELLO, rank, 0, &[])?;
332 let rd = stream.try_clone()?;
333 let inner2 = inner.clone();
334 readers.push(thread::spawn(move || reader_loop(rd, inner2)));
335 *inner.writers[p as usize].lock().unwrap() = Some(wr);
336 }
337
338 for _ in 0..rank {
340 let (stream, _addr) = listener.accept()?;
341 stream.set_nodelay(true).ok();
342 let mut rd = stream.try_clone()?;
343 let (kind, peer, _b, _payload) = read_frame(&mut rd)?;
344 if kind != HELLO || peer >= world {
345 return Err(io::Error::new(
346 io::ErrorKind::InvalidData,
347 format!("expected HELLO with valid rank, got kind={kind} rank={peer}"),
348 ));
349 }
350 let wr = stream.try_clone()?;
351 let inner2 = inner.clone();
352 readers.push(thread::spawn(move || reader_loop(rd, inner2)));
353 *inner.writers[peer as usize].lock().unwrap() = Some(wr);
354 }
355
356 Ok(Self {
357 inner,
358 readers: Mutex::new(readers),
359 })
360 }
361}
362
363impl NetTransport {
364 fn empty_inner(rank: u32, world: u32, heap_size: usize) -> Arc<NetInner> {
365 Arc::new(NetInner {
366 rank,
367 world,
368 heap_size,
369 writers: (0..world).map(|_| Mutex::new(None)).collect(),
370 inbox: Mutex::new(HashMap::new()),
371 inbox_cv: Condvar::new(),
372 heap: RwLock::new(vec![0u8; heap_size]),
373 getresp: Mutex::new(HashMap::new()),
374 getresp_cv: Condvar::new(),
375 shutdown: AtomicBool::new(false),
376 })
377 }
378
379 pub fn coordinator_listen(
385 world: u32,
386 listener: TcpListener,
387 heap_size: usize,
388 ) -> io::Result<Self> {
389 let inner = Self::empty_inner(0, world, heap_size);
390 let mut readers = Vec::new();
391 for _ in 1..world {
392 let (stream, _addr) = listener.accept()?;
393 stream.set_nodelay(true).ok();
394 let mut rd = stream.try_clone()?;
395 let (kind, peer, _b, _payload) = read_frame(&mut rd)?;
396 if kind != HELLO || peer == 0 || peer >= world {
397 return Err(io::Error::new(
398 io::ErrorKind::InvalidData,
399 format!("expected HELLO from a worker rank, got kind={kind} rank={peer}"),
400 ));
401 }
402 let wr = stream.try_clone()?;
403 let inner2 = inner.clone();
404 readers.push(thread::spawn(move || reader_loop(rd, inner2)));
405 *inner.writers[peer as usize].lock().unwrap() = Some(wr);
406 }
407 Ok(Self {
408 inner,
409 readers: Mutex::new(readers),
410 })
411 }
412
413 pub fn worker_dial(
417 rank: u32,
418 world: u32,
419 coord_addr: SocketAddr,
420 heap_size: usize,
421 ) -> io::Result<Self> {
422 assert!(rank > 0 && rank < world, "worker rank must be in 1..world");
423 let inner = Self::empty_inner(rank, world, heap_size);
424 let stream = connect_retry(coord_addr)?;
425 stream.set_nodelay(true).ok();
426 let mut wr = stream.try_clone()?;
427 write_frame(&mut wr, HELLO, rank, 0, &[])?;
428 let rd = stream.try_clone()?;
429 let inner2 = inner.clone();
430 let readers = vec![thread::spawn(move || reader_loop(rd, inner2))];
431 *inner.writers[0].lock().unwrap() = Some(wr);
432 Ok(Self {
433 inner,
434 readers: Mutex::new(readers),
435 })
436 }
437}
438
439impl Drop for NetTransport {
440 fn drop(&mut self) {
441 self.inner.shutdown.store(true, Ordering::SeqCst);
442 for w in &self.inner.writers {
444 if let Some(s) = w.lock().unwrap().as_ref() {
445 let _ = s.shutdown(Shutdown::Both);
446 }
447 }
448 self.inner.inbox_cv.notify_all();
449 self.inner.getresp_cv.notify_all();
450 if let Ok(mut hs) = self.readers.lock() {
451 for h in hs.drain(..) {
452 let _ = h.join();
453 }
454 }
455 }
456}
457
458impl Transport for NetTransport {
459 fn rank(&self) -> u32 {
460 self.inner.rank
461 }
462 fn world_size(&self) -> u32 {
463 self.inner.world
464 }
465 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
466 if to == self.inner.rank {
467 self.inner
469 .inbox
470 .lock()
471 .unwrap()
472 .entry((to, tag))
473 .or_default()
474 .push_back(bytes.to_vec());
475 self.inner.inbox_cv.notify_all();
476 return Ok(());
477 }
478 self.inner.send_frame(to, SEND, self.inner.rank, tag, bytes)
479 }
480 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
481 Ok(self.inner.recv_inbox(from, tag))
482 }
483 fn recv_bytes_timeout(
484 &self,
485 from: u32,
486 tag: u32,
487 timeout: Duration,
488 ) -> Result<Option<Vec<u8>>, CollectiveError> {
489 Ok(self.inner.recv_inbox_timeout(from, tag, timeout))
490 }
491 fn barrier(&self) -> Result<(), CollectiveError> {
492 default_barrier(self)
493 }
494}
495
496impl SymmetricTransport for NetTransport {
497 fn num_ranks(&self) -> u32 {
498 self.inner.world
499 }
500 fn this_rank(&self) -> Rank {
501 Rank(self.inner.rank)
502 }
503 fn put(&self, buf: SymmetricBuffer, src: &[u8]) -> Result<(), CollectiveError> {
504 self.inner.check_buf(buf)?;
505 if src.len() != buf.len {
506 return Err(CollectiveError::LengthMismatch {
507 expected: buf.len,
508 got: src.len(),
509 });
510 }
511 if buf.rank.0 == self.inner.rank {
512 let mut h = self.inner.heap.write().unwrap();
513 h[buf.offset..buf.offset + buf.len].copy_from_slice(src);
514 Ok(())
515 } else {
516 self.inner
517 .send_frame(buf.rank.0, PUT, self.inner.rank, buf.offset as u32, src)
518 }
519 }
520 fn get(&self, buf: SymmetricBuffer, dst: &mut [u8]) -> Result<(), CollectiveError> {
521 self.inner.check_buf(buf)?;
522 if dst.len() != buf.len {
523 return Err(CollectiveError::LengthMismatch {
524 expected: buf.len,
525 got: dst.len(),
526 });
527 }
528 if buf.rank.0 == self.inner.rank {
529 let h = self.inner.heap.read().unwrap();
530 dst.copy_from_slice(&h[buf.offset..buf.offset + buf.len]);
531 Ok(())
532 } else {
533 let mut payload = [0u8; 8];
534 payload[0..4].copy_from_slice(&(buf.offset as u32).to_le_bytes());
535 payload[4..8].copy_from_slice(&(buf.len as u32).to_le_bytes());
536 self.inner
537 .send_frame(buf.rank.0, GETREQ, self.inner.rank, 0, &payload)?;
538 let data = self.inner.wait_getresp(buf.rank.0);
539 if data.len() != buf.len {
540 return Err(CollectiveError::LengthMismatch {
541 expected: buf.len,
542 got: data.len(),
543 });
544 }
545 dst.copy_from_slice(&data);
546 Ok(())
547 }
548 }
549 fn barrier(&self) -> Result<(), CollectiveError> {
550 default_barrier(self)
551 }
552}
553
554pub const DEFAULT_HEAP_BYTES: usize = 64 * 1024 * 1024;
558
559pub struct TcpTransport;
563
564impl TcpTransport {
565 pub fn bind(
568 rank: u32,
569 world: u32,
570 peers: Vec<SocketAddr>,
571 heap_size: usize,
572 ) -> io::Result<NetTransport> {
573 let listener = TcpListener::bind(peers[rank as usize])?;
574 NetTransport::from_listener(rank, world, listener, peers, heap_size)
575 }
576}
577
578pub struct ThunderboltTransport;
584
585impl ThunderboltTransport {
586 pub fn bind(
590 rank: u32,
591 world: u32,
592 peers: Vec<SocketAddr>,
593 heap_size: usize,
594 ) -> io::Result<NetTransport> {
595 TcpTransport::bind(rank, world, peers, heap_size)
596 }
597
598 pub fn looks_like_thunderbolt(ip: IpAddr) -> bool {
603 match ip {
604 IpAddr::V4(v4) => {
605 let o = v4.octets();
606 o[0] == 169 && o[1] == 254 || o[0] == 10
607 }
608 IpAddr::V6(_) => false,
609 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use crate::collective::{ReduceKind, all_reduce};
617 use crate::transport::ProcessGroup;
618
619 fn run_net<F>(n: u32, heap: usize, body: F)
622 where
623 F: Fn(u32, Arc<NetTransport>) + Send + Sync + 'static,
624 {
625 let listeners: Vec<TcpListener> = (0..n)
626 .map(|_| TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap())
627 .collect();
628 let addrs: Vec<SocketAddr> = listeners.iter().map(|l| l.local_addr().unwrap()).collect();
629 let body = Arc::new(body);
630 let handles: Vec<_> = listeners
631 .into_iter()
632 .enumerate()
633 .map(|(rank, listener)| {
634 let addrs = addrs.clone();
635 let body = body.clone();
636 thread::spawn(move || {
637 let t = Arc::new(
638 NetTransport::from_listener(rank as u32, n, listener, addrs, heap)
639 .expect("transport build"),
640 );
641 body(rank as u32, t.clone());
642 <NetTransport as Transport>::barrier(&*t).unwrap();
646 })
647 })
648 .collect();
649 for h in handles {
650 h.join().unwrap();
651 }
652 }
653
654 fn run_star<F>(n: u32, heap: usize, body: F)
657 where
658 F: Fn(u32, Arc<NetTransport>) + Send + Sync + 'static,
659 {
660 let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap();
661 let coord_addr = listener.local_addr().unwrap();
662 let body = Arc::new(body);
663 let mut handles = Vec::new();
664 {
665 let body = body.clone();
666 handles.push(thread::spawn(move || {
667 let t = Arc::new(NetTransport::coordinator_listen(n, listener, heap).unwrap());
668 body(0, t.clone());
669 <NetTransport as Transport>::barrier(&*t).unwrap();
670 }));
671 }
672 for rank in 1..n {
673 let body = body.clone();
674 handles.push(thread::spawn(move || {
675 let t = Arc::new(NetTransport::worker_dial(rank, n, coord_addr, heap).unwrap());
676 body(rank, t.clone());
677 <NetTransport as Transport>::barrier(&*t).unwrap();
678 }));
679 }
680 for h in handles {
681 h.join().unwrap();
682 }
683 }
684
685 #[test]
686 fn star_dial_out_coordinator_worker() {
687 run_star(3, 4096, |rank, t| {
690 let g = ProcessGroup::new(t);
691 if rank == 0 {
692 let a = g.recv_f32(1, 5).unwrap();
693 let b = g.recv_f32(2, 5).unwrap();
694 let sum: Vec<f32> = a.iter().zip(&b).map(|(x, y)| x + y).collect();
695 g.send_f32(1, 6, &sum).unwrap();
696 g.send_f32(2, 6, &sum).unwrap();
697 } else {
698 g.send_f32(0, 5, &[rank as f32]).unwrap();
699 assert_eq!(g.recv_f32(0, 6).unwrap(), vec![3.0]); }
701 });
702 }
703
704 #[test]
705 fn two_sided_pipeline_handoff() {
706 run_net(3, 4096, |rank, t| {
709 let g = ProcessGroup::new(t);
710 let n = g.world_size();
711 if rank + 1 < n {
712 let got = g.recv_f32(rank + 1, 7).unwrap();
713 assert_eq!(got, vec![(rank as f32 + 1.0) * 10.0, 1.0]);
714 }
715 if rank > 0 {
716 g.send_f32(rank - 1, 7, &[rank as f32 * 10.0, 1.0]).unwrap();
717 }
718 });
719 }
720
721 #[test]
722 fn process_group_all_reduce_over_tcp() {
723 run_net(4, 4096, |rank, t| {
724 let g = ProcessGroup::new(t);
725 let mut data = vec![rank as f32 + 1.0; 5];
726 g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
727 assert_eq!(data, vec![10.0; 5], "rank {rank}");
728 });
729 }
730
731 #[test]
732 fn federated_average_skips_late_worker() {
733 run_net(3, 4096, |rank, t| {
736 let g = ProcessGroup::new(t);
737 let mut data = vec![rank as f32 + 1.0; 3]; if rank == 2 {
739 std::thread::sleep(Duration::from_millis(300));
740 }
741 let present = g
742 .federated_average(&mut data, Duration::from_millis(100))
743 .unwrap();
744 if rank == 0 {
745 assert_eq!(present, 2, "late rank 2 must be dropped");
746 assert_eq!(data, vec![1.5; 3]); }
748 });
749 }
750
751 #[test]
752 fn process_group_barrier_and_broadcast() {
753 run_net(3, 4096, |rank, t| {
754 let g = ProcessGroup::new(t);
755 g.barrier().unwrap();
756 let mut data = if rank == 0 {
757 vec![1.0, 2.0]
758 } else {
759 vec![0.0, 0.0]
760 };
761 g.broadcast(0, &mut data).unwrap();
762 assert_eq!(data, vec![1.0, 2.0], "rank {rank}");
763 });
764 }
765
766 #[test]
767 fn symmetric_remote_put_get() {
768 run_net(2, 4096, |rank, t| {
771 let off = 128;
772 let payload = [5u8, 6, 7, 8];
773 if rank == 0 {
774 t.put(
776 SymmetricBuffer {
777 rank: Rank(0),
778 offset: off,
779 len: 4,
780 },
781 &[1u8, 2, 3, 4],
782 )
783 .unwrap();
784 t.put(
786 SymmetricBuffer {
787 rank: Rank(1),
788 offset: off,
789 len: 4,
790 },
791 &payload,
792 )
793 .unwrap();
794 }
795 <NetTransport as Transport>::barrier(&t).unwrap();
797 if rank == 1 {
799 let mut got = [0u8; 4];
800 t.get(
801 SymmetricBuffer {
802 rank: Rank(1),
803 offset: off,
804 len: 4,
805 },
806 &mut got,
807 )
808 .unwrap();
809 assert_eq!(got, payload, "local read of remote PUT");
810 let mut remote = [0u8; 4];
811 t.get(
812 SymmetricBuffer {
813 rank: Rank(0),
814 offset: off,
815 len: 4,
816 },
817 &mut remote,
818 )
819 .unwrap();
820 assert_eq!(remote, [1u8, 2, 3, 4], "remote GET from rank 0");
821 }
822 });
823 }
824
825 #[test]
826 fn symmetric_collective_all_reduce_over_net() {
827 run_net(4, 4096, |rank, t| {
830 let elems = 3usize;
831 let bytes = elems * 4;
832 let buf = SymmetricBuffer {
833 rank: Rank(rank),
834 offset: 0,
835 len: bytes,
836 };
837 let mut local = vec![rank as f32 + 1.0; elems];
838 all_reduce(&*t, buf, &mut local, ReduceKind::Sum).unwrap();
839 assert_eq!(local, vec![10.0; elems], "rank {rank}");
840 });
841 }
842
843 #[test]
844 fn thunderbolt_addr_heuristic() {
845 use std::net::Ipv4Addr;
846 assert!(ThunderboltTransport::looks_like_thunderbolt(
847 Ipv4Addr::new(169, 254, 3, 1).into()
848 ));
849 assert!(ThunderboltTransport::looks_like_thunderbolt(
850 Ipv4Addr::new(10, 0, 0, 2).into()
851 ));
852 assert!(!ThunderboltTransport::looks_like_thunderbolt(
853 Ipv4Addr::new(192, 168, 1, 5).into()
854 ));
855 }
856}