ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
//! Privilege-free parity tests for the in-memory dummy datalink backend.
//!
//! These mirror libpnet's `pnet_datalink::dummy` test suite (send_nothing / send_one /
//! send_multiple / read_one / read_multiple / idle-blocks) against ferranet's *real*
//! `Sender`/`Receiver` API, plus error-injection and end-of-stream cases. They need no
//! `CAP_NET_RAW`, no `veth`, and no namespace, so they run in ordinary `cargo test`.

use std::io;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use ferranet::dummy::{self, DummyChannel};

fn frame(tag: u8, len: usize) -> Vec<u8> {
    vec![tag; len]
}

#[test]
fn send_nothing() {
    let mut net = dummy::channel().expect("dummy channel");
    let sent = net.tx.send_batch(&[]).expect("send empty batch");
    assert_eq!(sent, 0);
    assert!(net.drain.try_recv().is_none(), "no frame should have been transmitted");
}

#[test]
fn send_one_frame() {
    let mut net = dummy::channel().expect("dummy channel");
    let f = frame(9, 20);
    net.tx.send(&f).expect("send");
    assert_eq!(net.drain.try_recv().expect("one frame transmitted"), f);
    assert!(net.drain.try_recv().is_none(), "exactly one frame");
}

#[test]
fn send_multiple_frames_in_order() {
    let mut net = dummy::channel().expect("dummy channel");
    let frames = [frame(0, 20), frame(1, 20), frame(2, 20)];
    let refs: Vec<&[u8]> = frames.iter().map(Vec::as_slice).collect();
    assert_eq!(net.tx.send_batch(&refs).expect("send batch"), 3);
    for i in 0..3u8 {
        assert_eq!(net.drain.try_recv().expect("a frame")[0], i);
    }
    assert!(net.drain.try_recv().is_none());
}

#[test]
fn recv_one_frame() {
    let mut net = dummy::channel().expect("dummy channel");
    net.inject.inject(frame(7, 20));
    let block = net.rx.recv_block().expect("recv");
    assert_eq!(block.len(), 1);
    let f = block.frames().next().expect("a frame");
    assert_eq!(f.data().len(), 20);
    assert_eq!(f.data()[0], 7);
}

#[test]
fn recv_multiple_frames_in_order() {
    let mut net = dummy::channel().expect("dummy channel");
    for i in 0..3u8 {
        net.inject.inject(frame(i, 20));
    }
    for i in 0..3u8 {
        let block = net.rx.recv_block().expect("recv");
        assert_eq!(block.frames().next().expect("a frame").data()[0], i);
    }
}

#[test]
fn round_trip_send_and_receive() {
    let mut net = dummy::channel().expect("dummy channel");
    net.inject.inject(frame(0xab, 64));
    net.tx.send(&frame(0xcd, 64)).expect("send");

    let block = net.rx.recv_block().expect("recv");
    assert_eq!(block.frames().next().unwrap().data()[0], 0xab);
    assert_eq!(net.drain.try_recv().unwrap()[0], 0xcd);
}

#[test]
fn recv_blocks_when_idle_but_connected() {
    // Keep `inject` alive (network connected) but never inject: recv_block must block.
    let DummyChannel { mut rx, inject, .. } = dummy::channel().expect("dummy channel");
    let (done_tx, done_rx) = mpsc::channel();
    thread::spawn(move || {
        let _ = rx.recv_block(); // blocks forever on an idle, connected network
        let _ = done_tx.send(());
    });
    assert!(
        matches!(done_rx.recv_timeout(Duration::from_millis(150)), Err(mpsc::RecvTimeoutError::Timeout)),
        "recv_block should not return while the network is idle",
    );
    drop(inject);
}

#[test]
fn recv_propagates_injected_error() {
    let mut net = dummy::channel().expect("dummy channel");
    net.inject.inject_error(io::Error::other("simulated rx error"));
    let err = net.rx.recv_block().expect_err("error should propagate");
    assert!(matches!(err, ferranet::Error::Recv(_)), "got {err:?}");
    assert_eq!(err.as_io().expect("carries the OS error").to_string(), "simulated rx error");
}

#[test]
fn recv_returns_eof_when_injector_dropped() {
    let DummyChannel { mut rx, inject, .. } = dummy::channel().expect("dummy channel");
    drop(inject); // close the network with nothing queued
    let err = rx.recv_block().expect_err("closed network should error");
    match err {
        ferranet::Error::Recv(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof),
        other => panic!("expected UnexpectedEof, got {other:?}"),
    }
}

#[test]
fn drains_queued_frames_before_eof() {
    // Frames injected before the injector is dropped are still delivered; only then EOF.
    let DummyChannel { mut rx, inject, .. } = dummy::channel().expect("dummy channel");
    inject.inject(frame(1, 16));
    inject.inject(frame(2, 16));
    drop(inject);

    assert_eq!(rx.recv_block().unwrap().frames().next().unwrap().data()[0], 1);
    assert_eq!(rx.recv_block().unwrap().frames().next().unwrap().data()[0], 2);
    assert!(matches!(rx.recv_block(), Err(ferranet::Error::Recv(_))));
}

#[test]
fn dummy_interfaces_listed() {
    let ifaces = dummy::interfaces();
    assert_eq!(ifaces.len(), 3);
    assert_eq!(ifaces[0].name, "dummy0");
    assert!(ifaces.iter().all(|i| i.mac.is_some()));
}

/// The async surface of the dummy backend, mirroring the sync tests above. Also privilege-free.
#[cfg(feature = "tokio")]
mod async_dummy {
    use std::time::Duration;

    #[tokio::test]
    async fn async_round_trip() {
        let mut net = ferranet::dummy::channel_async().expect("async dummy channel");

        net.inject.inject(b"incoming".to_vec());
        let block = net.rx.recv_block().await.expect("recv injected frame");
        assert_eq!(block.len(), 1);
        assert_eq!(block.frames().next().expect("one frame").data(), b"incoming");
        drop(block);

        net.tx.send(b"outgoing").await.expect("send frame");
        assert_eq!(net.drain.try_recv().expect("one sent frame"), b"outgoing");
    }

    #[tokio::test]
    async fn async_send_batch_counts() {
        let mut net = ferranet::dummy::channel_async().expect("async dummy channel");
        let n = net.tx.send_batch(&[b"a".as_slice(), b"b".as_slice()]).await.expect("batch");
        assert_eq!(n, 2);
        assert_eq!(net.drain.len(), 2);
    }

    #[tokio::test]
    async fn async_recv_eof_when_injector_dropped() {
        let mut net = ferranet::dummy::channel_async().expect("async dummy channel");
        drop(net.inject);
        let err = net.rx.recv_block().await.expect_err("closed network is end-of-stream");
        assert!(matches!(
            err,
            ferranet::Error::Recv(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof
        ));
    }

    #[tokio::test]
    async fn async_recv_cancellation_is_safe() {
        let mut net = ferranet::dummy::channel_async().expect("async dummy channel");

        // Poll a recv future while the queue is empty, then drop it mid-wait.
        {
            let fut = net.rx.recv_block();
            tokio::pin!(fut);
            tokio::select! {
                biased;
                _ = &mut fut => panic!("nothing was injected yet"),
                _ = tokio::time::sleep(Duration::from_millis(50)) => {}
            }
        }

        // A frame injected after the cancellation must still be delivered promptly.
        net.inject.inject(b"after-cancel".to_vec());
        let block = tokio::time::timeout(Duration::from_secs(1), net.rx.recv_block())
            .await
            .expect("recv after a cancelled future must not hang")
            .expect("recv frame");
        assert_eq!(block.frames().next().expect("one frame").data(), b"after-cancel");
    }
}