use super::super::super::*;
use super::*;
#[test]
fn port1_tx_single_descriptor_lands_in_port1_buf() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port1 single descriptor TX";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained,
payload.to_vec(),
"port 1 TX must deliver the descriptor's bytes to drain_bulk verbatim",
);
assert!(
dev.drain_output().is_empty(),
"port 0 TX buffer must remain empty when only port 1 was notified",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "exactly one used-ring entry expected");
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"INT_VRING must be set after a successful TX drain",
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used",
);
}
#[test]
fn control_tx_publishes_zero_used_len() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let ctrl: [u8; VC_CONTROL_SIZE] = [2, 0, 0, 0, 6, 0, 1, 0];
mem.write_slice(&ctrl, data_addr)
.expect("plant control msg");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
VC_CONTROL_SIZE as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, C_OVQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, C_OVQ as u32);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "c_ovq chain must be add_used'd exactly once");
let used_elem_len: u32 = mem
.read_obj(mock.used_addr().checked_add(8).unwrap())
.expect("read used elem 0 len");
assert_eq!(
used_elem_len, 0,
"c_ovq add_used len must be 0 (device-readable control-TX \
buffer, virtio v1.2 §2.7.8.2); pre-fix it published the \
bytes-consumed total",
);
}
#[test]
fn port1_tx_multi_descriptor_chain_concatenates() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
const PAGE: u32 = 4096;
let segs: [(GuestAddress, u8); 4] = [
(GuestAddress(0x10000), 0xA1),
(GuestAddress(0x12000), 0xA2),
(GuestAddress(0x14000), 0xA3),
(GuestAddress(0x16000), 0xA4),
];
for (addr, fill) in &segs {
let buf = vec![*fill; PAGE as usize];
mem.write_slice(&buf, *addr).expect("plant segment");
}
let descs = [
RawDescriptor::from(SplitDescriptor::new(segs[0].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[1].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[2].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[3].0.0, PAGE, 0, 0)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained.len(),
4 * PAGE as usize,
"drain_bulk length must equal sum of segment lengths",
);
for (i, (_, fill)) in segs.iter().enumerate() {
let start = i * PAGE as usize;
let end = start + PAGE as usize;
assert!(
drained[start..end].iter().all(|&b| b == *fill),
"segment {i} must hold fill {fill:#x} verbatim — chain order \
or per-descriptor append regressed",
);
}
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 1,
"one chain → one used-ring entry, regardless of segment count",
);
}
#[test]
fn port1_tx_oversize_descriptor_truncates_to_tx_desc_max() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
const OVERSIZE: usize = TX_DESC_MAX * 2;
let mut payload = vec![0x55u8; TX_DESC_MAX];
payload.extend_from_slice(&vec![0x99u8; TX_DESC_MAX]);
assert_eq!(payload.len(), OVERSIZE);
mem.write_slice(&payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
OVERSIZE as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained.len(),
TX_DESC_MAX,
"oversize descriptor (len > TX_DESC_MAX) must truncate to TX_DESC_MAX",
);
assert!(
drained.iter().all(|&b| b == 0x55),
"truncated bytes must be the FIRST TX_DESC_MAX bytes \
of the descriptor (0x55), not anything past the cap",
);
}
#[test]
fn port1_tx_rejected_without_driver_ok() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"this must NOT reach port1_tx_buf";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
assert_eq!(
dev.device_status & VIRTIO_CONFIG_S_DRIVER_OK,
0,
"precondition: DRIVER_OK must NOT be set",
);
assert!(
dev.queues[PORT1_TXQ].ready(),
"precondition: port 1 TX queue must be ready (the gate \
we are testing is the DRIVER_OK gate, not a not-ready \
gate)",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
assert!(
dev.drain_bulk().is_empty(),
"port1_tx_buf must remain empty — DRIVER_OK gate must \
reject pre-DRIVER_OK notify",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"used.idx must be 0 — DRIVER_OK gate must skip add_used",
);
assert_eq!(
dev.interrupt_status, 0,
"interrupt_status must be 0 — DRIVER_OK gate must skip signal_used",
);
match dev.irq_evt.read() {
Ok(n) => panic!("irq_evt must NOT have been written, but read returned {n}"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error: {e}"),
}
}
#[test]
fn port0_tx_vs_port1_tx_routes_to_correct_buffer() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock0 = MockSplitQueue::create(&mem, GuestAddress(0x0), 16);
let mock1 = MockSplitQueue::create(&mem, GuestAddress(0x1000), 16);
let port0_data_addr = GuestAddress(0x10000);
let port1_data_addr = GuestAddress(0x20000);
let port0_payload = b"port0 console bytes";
let port1_payload = b"port1 bulk TLV bytes";
mem.write_slice(port0_payload, port0_data_addr)
.expect("plant port0 payload");
mem.write_slice(port1_payload, port1_data_addr)
.expect("plant port1 payload");
let port0_descs = [RawDescriptor::from(SplitDescriptor::new(
port0_data_addr.0,
port0_payload.len() as u32,
0,
0,
))];
let port1_descs = [RawDescriptor::from(SplitDescriptor::new(
port1_data_addr.0,
port1_payload.len() as u32,
0,
0,
))];
mock0
.build_desc_chain(&port0_descs)
.expect("build port0 chain");
mock1
.build_desc_chain(&port1_descs)
.expect("build port1 chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT0_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let d0 = mock0.desc_table_addr().0;
let a0 = mock0.avail_addr().0;
let u0 = mock0.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, d0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (d0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, a0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (a0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, u0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (u0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let d1 = mock1.desc_table_addr().0;
let a1 = mock1.avail_addr().0;
let u1 = mock1.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, d1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (d1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, a1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (a1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, u1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (u1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"FSM did not reach DRIVER_OK after both queues configured",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT0_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let port0_drained = dev.drain_output();
assert_eq!(
port0_drained,
port0_payload.to_vec(),
"port 0 TX bytes must route to port0_tx_buf — drain_output \
returns port0 bytes verbatim",
);
let port1_drained = dev.drain_bulk();
assert_eq!(
port1_drained,
port1_payload.to_vec(),
"port 1 TX bytes must route to port1_tx_buf — drain_bulk \
returns port1 bytes verbatim",
);
let port0_used_idx: u16 = mem
.read_obj(mock0.used_addr().checked_add(2).unwrap())
.expect("read port0 used.idx");
let port1_used_idx: u16 = mem
.read_obj(mock1.used_addr().checked_add(2).unwrap())
.expect("read port1 used.idx");
assert_eq!(
port0_used_idx, 1,
"port 0 used.idx must reflect 1 completion"
);
assert_eq!(
port1_used_idx, 1,
"port 1 used.idx must reflect 1 completion"
);
}
#[test]
fn port1_tx_per_call_cap_partial_drain() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
const N_CHAINS: usize = 9;
let mut descs: Vec<RawDescriptor> = Vec::with_capacity(N_CHAINS);
for i in 0..N_CHAINS {
let buf_addr = GuestAddress(0x10000 + (i as u64) * (TX_DESC_MAX as u64));
let fill = (i + 1) as u8;
let payload = vec![fill; TX_DESC_MAX];
mem.write_slice(&payload, buf_addr)
.expect("plant per-chain payload");
descs.push(RawDescriptor::from(SplitDescriptor::new(
buf_addr.0,
TX_DESC_MAX as u32,
0, 0,
)));
}
mock.add_desc_chains(&descs, 0)
.expect("publish 9 standalone chains");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained_first = dev.drain_bulk();
assert_eq!(
drained_first.len(),
TX_PER_CALL_MAX,
"first notify must drain exactly TX_PER_CALL_MAX bytes \
(8 × TX_DESC_MAX) — the per-call cap stops popping after \
the 8th chain"
);
for i in 0..8 {
let start = i * TX_DESC_MAX;
let end = start + TX_DESC_MAX;
let expected_fill = (i + 1) as u8;
assert!(
drained_first[start..end]
.iter()
.all(|&b| b == expected_fill),
"chain {i} bytes must be fill={expected_fill}; \
a regression that drained past the cap (or out of \
chain order) would surface a different byte here"
);
}
assert!(
!drained_first.contains(&9u8),
"9th chain (fill=9) must NOT appear in the first drain \
— the per-call cap must hold the 9th chain back"
);
let used_idx_first: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after first notify");
assert_eq!(
used_idx_first, 8,
"used.idx must be 8 after first notify — the cap stopped \
popping after the 8th chain so only 8 add_used calls \
happened"
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained_second = dev.drain_bulk();
assert_eq!(
drained_second.len(),
TX_DESC_MAX,
"second notify must drain the remaining 9th chain \
(TX_DESC_MAX bytes) — the cap is per-call, not per-run"
);
assert!(
drained_second.iter().all(|&b| b == 9u8),
"second drain must contain the 9th chain's bytes (fill=9)"
);
let used_idx_second: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after second notify");
assert_eq!(
used_idx_second, 9,
"used.idx must be 9 after second notify — every chain \
eventually drains, the cap only spreads them across \
multiple notifies"
);
}