From 71d66ab2b621bad85127d7e0a6f732d46ddf1e05 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Fri, 13 Mar 2026 17:17:24 -0700 Subject: [PATCH] test(serial): add tests for serial console Signed-off-by: Changyuan Lyu --- alioth/src/device/ioapic.rs | 2 +- alioth/src/device/ioapic_test.rs | 60 +++----- alioth/src/device/serial.rs | 32 +++-- alioth/src/device/serial_test.rs | 234 +++++++++++++++++++++++++++++++ alioth/src/hv/hv_test.rs | 20 ++- 5 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 alioth/src/device/serial_test.rs diff --git a/alioth/src/device/ioapic.rs b/alioth/src/device/ioapic.rs index f20feec4..c017596f 100644 --- a/alioth/src/device/ioapic.rs +++ b/alioth/src/device/ioapic.rs @@ -182,4 +182,4 @@ impl MmioDev for IoApic {} #[cfg(test)] #[path = "ioapic_test.rs"] -mod tests; +pub mod tests; diff --git a/alioth/src/device/ioapic_test.rs b/alioth/src/device/ioapic_test.rs index 20fd3d9e..3f861650 100644 --- a/alioth/src/device/ioapic_test.rs +++ b/alioth/src/device/ioapic_test.rs @@ -12,34 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; - use assert_matches::assert_matches; -use parking_lot::Mutex; -use crate::hv::tests::TestIrqFd; -use crate::hv::{Error as HvError, MsiSender}; +use crate::arch::x86_64::ioapic::{IOREGSEL, IOWIN, RedirectEntry}; +use crate::hv::MsiSender; +use crate::hv::tests::TestMsiSender; use crate::mem::emulated::Mmio; -use super::{IOREGSEL, IOWIN, IoApic}; - -#[derive(Debug, Default)] -struct TestMsiSender { - messages: Arc>>, -} - -impl MsiSender for TestMsiSender { - type IrqFd = TestIrqFd; - - fn send(&self, addr: u64, data: u32) -> Result<(), HvError> { - self.messages.lock().push((addr, data)); - Ok(()) - } - - fn create_irqfd(&self) -> Result { - Ok(TestIrqFd::default()) - } -} +use super::IoApic; #[test] fn test_ioapic_read_write() { @@ -69,25 +49,31 @@ fn test_ioapic_read_write() { assert_eq!(regs.redirtbl[0].0, 0xabcdef0012345678); } +/// Configure redirection table entry for pin `pin` with `vector` and `dest`. +/// physical, edge triggered. +pub(crate) fn enable_pin(io_apci: &IoApic, pin: u8, vector: u8, dest: u8) { + let mut redirtbl_entry = RedirectEntry(0); + redirtbl_entry.set_vector(vector); + redirtbl_entry.set_dest_id(dest); + // IOREDTBL for pin 4 is at registers 0x10 + pin * 2 + let offset = 0x10 + (pin as u64 * 2); + io_apci.write(IOREGSEL, 4, offset).unwrap(); + io_apci + .write(IOWIN, 4, (redirtbl_entry.0 & 0xffffffff) as u64) + .unwrap(); + io_apci.write(IOREGSEL, 4, offset + 1).unwrap(); + io_apci + .write(IOWIN, 4, (redirtbl_entry.0 >> 32) as u64) + .unwrap(); +} + #[test] fn test_ioapic_service_pin() { let msi_sender = TestMsiSender::default(); let messages = msi_sender.messages.clone(); let io_apic = IoApic::new(msi_sender); - // Configure redirection table entry for pin 4 - // Vector 0x24, destination 2, physical, edge triggered - let redirtbl_entry = (2u64 << 56) | 0x24; - - // IOREDTBL for pin 4 is at registers 0x10 + 4*2 = 0x18 and 0x19 - io_apic.write(IOREGSEL, 4, 0x18).unwrap(); - io_apic - .write(IOWIN, 4, (redirtbl_entry & 0xFFFFFFFF) as u64) - .unwrap(); - io_apic.write(IOREGSEL, 4, 0x19).unwrap(); - io_apic - .write(IOWIN, 4, (redirtbl_entry >> 32) as u64) - .unwrap(); + enable_pin(&io_apic, 4, 0x24, 2); // Service pin 4 io_apic.service_pin(4).unwrap(); diff --git a/alioth/src/device/serial.rs b/alioth/src/device/serial.rs index 18e8f44c..7db516d5 100644 --- a/alioth/src/device/serial.rs +++ b/alioth/src/device/serial.rs @@ -26,18 +26,18 @@ use crate::hv::MsiSender; use crate::mem::emulated::{Action, Mmio}; use crate::{bitflags, mem}; -const TX_HOLDING_REGISTER: u16 = 0x0; -const RX_BUFFER_REGISTER: u16 = 0x0; -const DIVISOR_LATCH_LSB: u16 = 0x0; -const DIVISOR_LATCH_MSB: u16 = 0x1; -const INTERRUPT_ENABLE_REGISTER: u16 = 0x1; -const FIFO_CONTROL_REGISTER: u16 = 0x2; -const INTERRUPT_IDENTIFICATION_REGISTER: u16 = 0x2; -const LINE_CONTROL_REGISTER: u16 = 0x3; -const MODEM_CONTROL_REGISTER: u16 = 0x4; -const LINE_STATUS_REGISTER: u16 = 0x5; -const MODEM_STATUS_REGISTER: u16 = 0x6; -const SCRATCH_REGISTER: u16 = 0x7; +const TX_HOLDING_REGISTER: u64 = 0x0; +const RX_BUFFER_REGISTER: u64 = 0x0; +const DIVISOR_LATCH_LSB: u64 = 0x0; +const DIVISOR_LATCH_MSB: u64 = 0x1; +const INTERRUPT_ENABLE_REGISTER: u64 = 0x1; +const FIFO_CONTROL_REGISTER: u64 = 0x2; +const INTERRUPT_IDENTIFICATION_REGISTER: u64 = 0x2; +const LINE_CONTROL_REGISTER: u64 = 0x3; +const MODEM_CONTROL_REGISTER: u64 = 0x4; +const LINE_STATUS_REGISTER: u64 = 0x5; +const MODEM_STATUS_REGISTER: u64 = 0x6; +const SCRATCH_REGISTER: u64 = 0x7; // offset 0x1, Interrupt Enable Register (IER) bitflags! { @@ -203,7 +203,7 @@ where fn read(&self, offset: u64, _size: u8) -> Result { let mut reg = self.reg.lock(); - let ret = match offset as u16 { + let ret = match offset { DIVISOR_LATCH_LSB if reg.line_control.divisor_latch_access() => reg.divisor as u8, DIVISOR_LATCH_MSB if reg.line_control.divisor_latch_access() => { (reg.divisor >> 8) as u8 @@ -236,7 +236,7 @@ where fn write(&self, offset: u64, _size: u8, val: u64) -> mem::Result { let byte = val as u8; let mut reg = self.reg.lock(); - match offset as u16 { + match offset { DIVISOR_LATCH_LSB if reg.line_control.divisor_latch_access() => { reg.divisor = (reg.divisor & 0xff00) | byte as u16; } @@ -365,3 +365,7 @@ where } } } + +#[cfg(test)] +#[path = "serial_test.rs"] +mod tests; diff --git a/alioth/src/device/serial_test.rs b/alioth/src/device/serial_test.rs new file mode 100644 index 00000000..e44dab9d --- /dev/null +++ b/alioth/src/device/serial_test.rs @@ -0,0 +1,234 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +use assert_matches::assert_matches; +use parking_lot::Mutex; + +use crate::device::console::tests::TestConsole; +use crate::device::ioapic::IoApic; +use crate::device::ioapic::tests::enable_pin; +use crate::device::serial::{ + DIVISOR_LATCH_LSB, DIVISOR_LATCH_MSB, FIFO_CONTROL_REGISTER, INTERRUPT_ENABLE_REGISTER, + INTERRUPT_IDENTIFICATION_REGISTER, LINE_CONTROL_REGISTER, LINE_STATUS_REGISTER, + MODEM_CONTROL_REGISTER, MODEM_STATUS_REGISTER, RX_BUFFER_REGISTER, SCRATCH_REGISTER, Serial, + TX_HOLDING_REGISTER, +}; +use crate::hv::tests::TestMsiSender; +use crate::mem::emulated::Mmio; + +fn fixture_serial() -> ( + Serial, + Arc>, + Arc>>, +) { + let msi_sender = TestMsiSender::default(); + let messages = msi_sender.messages.clone(); + let ioapic = Arc::new(IoApic::new(msi_sender)); + let console = TestConsole::new().unwrap(); + let serial = Serial::new(0x3f8, ioapic.clone(), 4, console).unwrap(); + (serial, ioapic, messages) +} + +#[test] +fn test_serial_basic() { + let (serial, _, _) = fixture_serial(); + + assert_eq!(serial.size(), 8); + + // Default LCR should be 0b00000011 (8 data bits) + assert_matches!(serial.read(LINE_CONTROL_REGISTER, 1), Ok(0x03)); + + // Write LCR to enable DLAB (Divisor Latch Access Bit) + assert_matches!(serial.write(LINE_CONTROL_REGISTER, 1, 0x83), Ok(_)); + assert_matches!(serial.read(LINE_CONTROL_REGISTER, 1), Ok(0x83)); + + // Write divisor latches + assert_matches!(serial.write(DIVISOR_LATCH_LSB as u64, 1, 0x12), Ok(_)); + assert_matches!(serial.write(DIVISOR_LATCH_MSB as u64, 1, 0x34), Ok(_)); + + // Read divisor latches + assert_matches!(serial.read(DIVISOR_LATCH_LSB as u64, 1), Ok(0x12)); + assert_matches!(serial.read(DIVISOR_LATCH_MSB as u64, 1), Ok(0x34)); + + // Disable DLAB + assert_matches!(serial.write(LINE_CONTROL_REGISTER, 1, 0x03), Ok(_)); + + // Scratch register + assert_matches!(serial.write(SCRATCH_REGISTER, 1, 0x5a), Ok(_)); + assert_matches!(serial.read(SCRATCH_REGISTER, 1), Ok(0x5a)); + + // Default IIR + assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x01)); + + // Modem Control Register + assert_matches!(serial.write(MODEM_CONTROL_REGISTER, 1, 0x1f), Ok(_)); + assert_matches!(serial.read(MODEM_CONTROL_REGISTER, 1), Ok(0x1f)); + + // Modem Status Register (read-only in real hardware, but we just check it returns 0 as it's uninitialized default) + assert_matches!(serial.read(MODEM_STATUS_REGISTER, 1), Ok(0x00)); + // Writing should be a no-op but shouldn't panic + assert_matches!(serial.write(MODEM_STATUS_REGISTER, 1, 0xff), Ok(_)); + + // FIFO Control Register (write-only) + assert_matches!(serial.write(FIFO_CONTROL_REGISTER, 1, 0xc7), Ok(_)); + + // Unreachable offsets + assert_matches!(serial.read(0x100, 1), Ok(0x00)); + assert_matches!(serial.write(0x100, 1, 0x00), Ok(_)); +} + +#[test] +fn test_serial_tx() { + let (serial, ioapic, messages) = fixture_serial(); + + // Enable TX empty interrupt + assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x02), Ok(_)); + + enable_pin(&ioapic, 4, 0x24, 2); + + // Write a character + assert_matches!(serial.write(TX_HOLDING_REGISTER, 1, b'A' as u64), Ok(_)); + + // Check if character is pushed to outbound console + let mut outbound = serial.console.outbound.lock(); + assert_eq!(outbound.pop_front(), Some(b'A')); + drop(outbound); + + // TX should send an IRQ through IOAPIC + let messages_lock = messages.lock(); + assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]); +} + +#[test] +fn test_serial_rx() { + let (serial, ioapic, messages) = fixture_serial(); + + // Enable RX available interrupt + assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x01), Ok(_)); + assert_matches!(serial.read(INTERRUPT_ENABLE_REGISTER, 4), Ok(0x01)); + + enable_pin(&ioapic, 4, 0x24, 2); + + { + serial.console.inbound.lock().push_back(b'B'); + serial.console.notifier.lock().notify().unwrap(); + } + + let now = Instant::now(); + while !matches!(serial.read(LINE_STATUS_REGISTER, 1), Ok(s) if s & 1 == 1) + && now.elapsed() < Duration::from_secs(5) + { + sleep(Duration::from_millis(100)); + } + + // Check if data is available + assert_matches!( + serial.read(LINE_STATUS_REGISTER, 1), + Ok(s) if s & 1 == 1 + ); + + // Check IIR for RX data available + assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x04)); + + // Read the character + assert_matches!( + serial.read(RX_BUFFER_REGISTER, 1), + Ok(b) if b == b'B' as u64 + ); + + // RX should send an IRQ through IOAPIC + let messages_lock = messages.lock(); + assert_eq!(messages_lock.len(), 1); + assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]); + + // IIR should be cleared after read + assert_matches!(serial.read(INTERRUPT_IDENTIFICATION_REGISTER, 1), Ok(0x01)); +} + +#[test] +fn test_serial_rx_no_interrupt() { + let (serial, _ioapic, messages) = fixture_serial(); + + // Disable all interrupts + assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x00), Ok(_)); + + { + serial.console.inbound.lock().push_back(b'B'); + serial.console.notifier.lock().notify().unwrap(); + } + + let now = Instant::now(); + while !matches!(serial.read(LINE_STATUS_REGISTER, 1), Ok(s) if s & 1 == 1) + && now.elapsed() < Duration::from_secs(5) + { + sleep(Duration::from_millis(100)); + } + + // Check if data is available + assert_matches!( + serial.read(LINE_STATUS_REGISTER, 1), + Ok(s) if s & 1 == 1 + ); + + // Read the character + assert_matches!( + serial.read(RX_BUFFER_REGISTER, 1), + Ok(b) if b == b'B' as u64 + ); + + // No IRQ should have been sent + let messages_lock = messages.lock(); + assert_eq!(messages_lock.len(), 0); +} + +#[test] +fn test_serial_loopback() { + let (serial, ioapic, messages) = fixture_serial(); + + // Enable RX available interrupt + assert_matches!(serial.write(INTERRUPT_ENABLE_REGISTER, 1, 0x01), Ok(_)); + + enable_pin(&ioapic, 4, 0x24, 2); + + // Enable loopback mode (bit 4) + assert_matches!(serial.write(MODEM_CONTROL_REGISTER, 1, 0x10), Ok(_)); + + // Write a character + assert_matches!(serial.write(TX_HOLDING_REGISTER, 1, b'C' as u64), Ok(_)); + + // The character should be looped back into RX + assert_matches!( + serial.read(LINE_STATUS_REGISTER, 1), + Ok(s) if s & 1 == 1 + ); + assert_matches!( + serial.read(RX_BUFFER_REGISTER, 1), + Ok(b) if b == b'C' as u64 + ); + + let messages_lock = messages.lock(); + assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]); +} + +#[test] +fn test_serial_pause_resume() { + use crate::device::Pause; + let (serial, _, _) = fixture_serial(); + assert_matches!(serial.pause(), Ok(())); + assert_matches!(serial.resume(), Ok(())); +} diff --git a/alioth/src/hv/hv_test.rs b/alioth/src/hv/hv_test.rs index e84c9ec4..46dc990e 100644 --- a/alioth/src/hv/hv_test.rs +++ b/alioth/src/hv/hv_test.rs @@ -16,7 +16,7 @@ use std::os::fd::{AsFd, BorrowedFd}; use parking_lot::{Condvar, Mutex, RwLock}; -use crate::hv::{IrqFd, IrqSender, Result}; +use crate::hv::{IrqFd, IrqSender, MsiSender, Result}; #[derive(Debug)] struct TestIrqFdInner { @@ -112,3 +112,21 @@ impl IrqSender for TestIrqSender { Ok(()) } } + +#[derive(Debug, Default)] +pub struct TestMsiSender { + pub messages: std::sync::Arc>>, +} + +impl MsiSender for TestMsiSender { + type IrqFd = TestIrqFd; + + fn send(&self, addr: u64, data: u32) -> std::result::Result<(), crate::hv::Error> { + self.messages.lock().push((addr, data)); + Ok(()) + } + + fn create_irqfd(&self) -> std::result::Result { + Ok(TestIrqFd::default()) + } +}