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