Skip to content

Commit ba42bd1

Browse files
committed
test(serial): add tests for serial console
Signed-off-by: Changyuan Lyu <changyuanl@google.com>
1 parent 8c7d8a3 commit ba42bd1

4 files changed

Lines changed: 316 additions & 24 deletions

File tree

alioth/src/device/ioapic_test.rs

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,13 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::sync::Arc;
16-
1715
use assert_matches::assert_matches;
18-
use parking_lot::Mutex;
1916

20-
use crate::hv::tests::TestIrqFd;
21-
use crate::hv::{Error as HvError, MsiSender};
17+
use crate::hv::tests::TestMsiSender;
2218
use crate::mem::emulated::Mmio;
2319

2420
use super::{IOREGSEL, IOWIN, IoApic};
2521

26-
#[derive(Debug, Default)]
27-
struct TestMsiSender {
28-
messages: Arc<Mutex<Vec<(u64, u32)>>>,
29-
}
30-
31-
impl MsiSender for TestMsiSender {
32-
type IrqFd = TestIrqFd;
33-
34-
fn send(&self, addr: u64, data: u32) -> Result<(), HvError> {
35-
self.messages.lock().push((addr, data));
36-
Ok(())
37-
}
38-
39-
fn create_irqfd(&self) -> Result<Self::IrqFd, HvError> {
40-
Ok(TestIrqFd::default())
41-
}
42-
}
43-
4422
#[test]
4523
fn test_ioapic_read_write() {
4624
let io_apic = IoApic::new(TestMsiSender::default());

alioth/src/device/serial.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,7 @@ where
365365
}
366366
}
367367
}
368+
369+
#[cfg(test)]
370+
#[path = "serial_test.rs"]
371+
mod tests;

alioth/src/device/serial_test.rs

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::sync::Arc;
16+
use std::thread::sleep;
17+
use std::time::{Duration, Instant};
18+
19+
use assert_matches::assert_matches;
20+
use parking_lot::Mutex;
21+
22+
use crate::device::console::tests::TestConsole;
23+
use crate::device::ioapic::IoApic;
24+
use crate::device::serial::{
25+
DIVISOR_LATCH_LSB, DIVISOR_LATCH_MSB, FIFO_CONTROL_REGISTER, INTERRUPT_ENABLE_REGISTER,
26+
INTERRUPT_IDENTIFICATION_REGISTER, LINE_CONTROL_REGISTER, LINE_STATUS_REGISTER,
27+
MODEM_CONTROL_REGISTER, MODEM_STATUS_REGISTER, RX_BUFFER_REGISTER, SCRATCH_REGISTER, Serial,
28+
TX_HOLDING_REGISTER,
29+
};
30+
use crate::hv::tests::TestMsiSender;
31+
use crate::mem::emulated::Mmio;
32+
33+
const IOREGSEL: u64 = 0x00;
34+
const IOWIN: u64 = 0x10;
35+
36+
fn fixture_serial() -> (
37+
Serial<TestMsiSender, TestConsole>,
38+
Arc<IoApic<TestMsiSender>>,
39+
Arc<Mutex<Vec<(u64, u32)>>>,
40+
) {
41+
let msi_sender = TestMsiSender::default();
42+
let messages = msi_sender.messages.clone();
43+
let ioapic = Arc::new(IoApic::new(msi_sender));
44+
let console = TestConsole::new().unwrap();
45+
let serial = Serial::new(0x3f8, ioapic.clone(), 4, console).unwrap();
46+
(serial, ioapic, messages)
47+
}
48+
49+
#[test]
50+
fn test_serial_basic() {
51+
let (serial, _, _) = fixture_serial();
52+
53+
assert_eq!(serial.size(), 8);
54+
55+
// Default LCR should be 0b00000011 (8 data bits)
56+
assert_matches!(serial.read(LINE_CONTROL_REGISTER as u64, 1), Ok(0x03));
57+
58+
// Write LCR to enable DLAB (Divisor Latch Access Bit)
59+
assert_matches!(serial.write(LINE_CONTROL_REGISTER as u64, 1, 0x83), Ok(_));
60+
assert_matches!(serial.read(LINE_CONTROL_REGISTER as u64, 1), Ok(0x83));
61+
62+
// Write divisor latches
63+
assert_matches!(serial.write(DIVISOR_LATCH_LSB as u64, 1, 0x12), Ok(_));
64+
assert_matches!(serial.write(DIVISOR_LATCH_MSB as u64, 1, 0x34), Ok(_));
65+
66+
// Read divisor latches
67+
assert_matches!(serial.read(DIVISOR_LATCH_LSB as u64, 1), Ok(0x12));
68+
assert_matches!(serial.read(DIVISOR_LATCH_MSB as u64, 1), Ok(0x34));
69+
70+
// Disable DLAB
71+
assert_matches!(serial.write(LINE_CONTROL_REGISTER as u64, 1, 0x03), Ok(_));
72+
73+
// Scratch register
74+
assert_matches!(serial.write(SCRATCH_REGISTER as u64, 1, 0x5a), Ok(_));
75+
assert_matches!(serial.read(SCRATCH_REGISTER as u64, 1), Ok(0x5a));
76+
77+
// Default IIR
78+
assert_matches!(
79+
serial.read(INTERRUPT_IDENTIFICATION_REGISTER as u64, 1),
80+
Ok(0x01)
81+
);
82+
83+
// Modem Control Register
84+
assert_matches!(serial.write(MODEM_CONTROL_REGISTER as u64, 1, 0x1f), Ok(_));
85+
assert_matches!(serial.read(MODEM_CONTROL_REGISTER as u64, 1), Ok(0x1f));
86+
87+
// Modem Status Register (read-only in real hardware, but we just check it returns 0 as it's uninitialized default)
88+
assert_matches!(serial.read(MODEM_STATUS_REGISTER as u64, 1), Ok(0x00));
89+
// Writing should be a no-op but shouldn't panic
90+
assert_matches!(serial.write(MODEM_STATUS_REGISTER as u64, 1, 0xff), Ok(_));
91+
92+
// FIFO Control Register (write-only)
93+
assert_matches!(serial.write(FIFO_CONTROL_REGISTER as u64, 1, 0xc7), Ok(_));
94+
95+
// Unreachable offsets
96+
assert_matches!(serial.read(0x100, 1), Ok(0x00));
97+
assert_matches!(serial.write(0x100, 1, 0x00), Ok(_));
98+
}
99+
100+
#[test]
101+
fn test_serial_tx() {
102+
let (serial, ioapic, messages) = fixture_serial();
103+
104+
// Enable TX empty interrupt
105+
assert_matches!(
106+
serial.write(INTERRUPT_ENABLE_REGISTER as u64, 1, 0x02),
107+
Ok(_)
108+
);
109+
110+
// Configure IOAPIC to pass through IRQs
111+
// Vector 0x24, destination 2, physical, edge triggered
112+
let redirtbl_entry = (2u64 << 56) | 0x24;
113+
114+
// IOREDTBL for pin 4 is at registers 0x10 + 4*2 = 0x18 and 0x19
115+
ioapic.write(IOREGSEL, 4, 0x18).unwrap();
116+
ioapic
117+
.write(IOWIN, 4, (redirtbl_entry & 0xFFFFFFFF) as u64)
118+
.unwrap();
119+
ioapic.write(IOREGSEL, 4, 0x19).unwrap();
120+
ioapic
121+
.write(IOWIN, 4, (redirtbl_entry >> 32) as u64)
122+
.unwrap();
123+
124+
// Write a character
125+
assert_matches!(
126+
serial.write(TX_HOLDING_REGISTER as u64, 1, b'A' as u64),
127+
Ok(_)
128+
);
129+
130+
// Check if character is pushed to outbound console
131+
let mut outbound = serial.console.outbound.lock();
132+
assert_eq!(outbound.pop_front(), Some(b'A'));
133+
drop(outbound);
134+
135+
// TX should send an IRQ through IOAPIC
136+
let messages_lock = messages.lock();
137+
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);
138+
}
139+
140+
#[test]
141+
fn test_serial_rx() {
142+
let (serial, ioapic, messages) = fixture_serial();
143+
144+
// Enable RX available interrupt
145+
assert_matches!(
146+
serial.write(INTERRUPT_ENABLE_REGISTER as u64, 1, 0x01),
147+
Ok(_)
148+
);
149+
150+
// Configure IOAPIC to pass through IRQs
151+
let redirtbl_entry = (2u64 << 56) | 0x24;
152+
ioapic.write(IOREGSEL, 4, 0x18).unwrap();
153+
ioapic
154+
.write(IOWIN, 4, (redirtbl_entry & 0xFFFFFFFF) as u64)
155+
.unwrap();
156+
ioapic.write(IOREGSEL, 4, 0x19).unwrap();
157+
ioapic
158+
.write(IOWIN, 4, (redirtbl_entry >> 32) as u64)
159+
.unwrap();
160+
161+
{
162+
serial.console.inbound.lock().push_back(b'B');
163+
serial.console.notifier.lock().notify().unwrap();
164+
}
165+
166+
let now = Instant::now();
167+
while !matches!(serial.read(LINE_STATUS_REGISTER as u64, 1), Ok(s) if s & 1 == 1)
168+
&& now.elapsed() < Duration::from_secs(5)
169+
{
170+
sleep(Duration::from_millis(100));
171+
}
172+
173+
// Check if data is available
174+
assert_matches!(
175+
serial.read(LINE_STATUS_REGISTER as u64, 1),
176+
Ok(s) if s & 1 == 1
177+
);
178+
179+
// Check IIR for RX data available
180+
assert_matches!(
181+
serial.read(INTERRUPT_IDENTIFICATION_REGISTER as u64, 1),
182+
Ok(0x04)
183+
);
184+
185+
// Read the character
186+
assert_matches!(
187+
serial.read(RX_BUFFER_REGISTER as u64, 1),
188+
Ok(b) if b == b'B' as u64
189+
);
190+
191+
// RX should send an IRQ through IOAPIC
192+
let messages_lock = messages.lock();
193+
assert_eq!(messages_lock.len(), 1);
194+
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);
195+
196+
// IIR should be cleared after read
197+
assert_matches!(
198+
serial.read(INTERRUPT_IDENTIFICATION_REGISTER as u64, 1),
199+
Ok(0x01)
200+
);
201+
}
202+
203+
#[test]
204+
fn test_serial_rx_no_interrupt() {
205+
let (serial, _ioapic, messages) = fixture_serial();
206+
207+
// Disable all interrupts
208+
assert_matches!(
209+
serial.write(INTERRUPT_ENABLE_REGISTER as u64, 1, 0x00),
210+
Ok(_)
211+
);
212+
213+
{
214+
serial.console.inbound.lock().push_back(b'B');
215+
serial.console.notifier.lock().notify().unwrap();
216+
}
217+
218+
let now = Instant::now();
219+
while !matches!(serial.read(LINE_STATUS_REGISTER as u64, 1), Ok(s) if s & 1 == 1)
220+
&& now.elapsed() < Duration::from_secs(5)
221+
{
222+
sleep(Duration::from_millis(100));
223+
}
224+
225+
// Check if data is available
226+
assert_matches!(
227+
serial.read(LINE_STATUS_REGISTER as u64, 1),
228+
Ok(s) if s & 1 == 1
229+
);
230+
231+
// Read the character
232+
assert_matches!(
233+
serial.read(RX_BUFFER_REGISTER as u64, 1),
234+
Ok(b) if b == b'B' as u64
235+
);
236+
237+
// No IRQ should have been sent
238+
let messages_lock = messages.lock();
239+
assert_eq!(messages_lock.len(), 0);
240+
}
241+
242+
#[test]
243+
fn test_serial_loopback() {
244+
let (serial, ioapic, messages) = fixture_serial();
245+
246+
// Enable RX available interrupt
247+
assert_matches!(
248+
serial.write(INTERRUPT_ENABLE_REGISTER as u64, 1, 0x01),
249+
Ok(_)
250+
);
251+
252+
// Configure IOAPIC
253+
let redirtbl_entry = (2u64 << 56) | 0x24;
254+
ioapic.write(IOREGSEL, 4, 0x18).unwrap();
255+
ioapic
256+
.write(IOWIN, 4, (redirtbl_entry & 0xFFFFFFFF) as u64)
257+
.unwrap();
258+
ioapic.write(IOREGSEL, 4, 0x19).unwrap();
259+
ioapic
260+
.write(IOWIN, 4, (redirtbl_entry >> 32) as u64)
261+
.unwrap();
262+
263+
// Enable loopback mode (bit 4)
264+
assert_matches!(serial.write(MODEM_CONTROL_REGISTER as u64, 1, 0x10), Ok(_));
265+
266+
// Write a character
267+
assert_matches!(
268+
serial.write(TX_HOLDING_REGISTER as u64, 1, b'C' as u64),
269+
Ok(_)
270+
);
271+
272+
// The character should be looped back into RX
273+
assert_matches!(
274+
serial.read(LINE_STATUS_REGISTER as u64, 1),
275+
Ok(s) if s & 1 == 1
276+
);
277+
assert_matches!(
278+
serial.read(RX_BUFFER_REGISTER as u64, 1),
279+
Ok(b) if b == b'C' as u64
280+
);
281+
282+
let messages_lock = messages.lock();
283+
assert_matches!(messages_lock.as_slice(), [(0xfee02000, 0x24)]);
284+
}
285+
286+
#[test]
287+
fn test_serial_pause_resume() {
288+
use crate::device::Pause;
289+
let (serial, _, _) = fixture_serial();
290+
assert_matches!(serial.pause(), Ok(()));
291+
assert_matches!(serial.resume(), Ok(()));
292+
}

alioth/src/hv/hv_test.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::os::fd::{AsFd, BorrowedFd};
2020

2121
use parking_lot::RwLock;
2222

23-
use crate::hv::{IrqFd, Result};
23+
use crate::hv::{IrqFd, MsiSender, Result};
2424

2525
#[derive(Debug)]
2626
struct TestIrqFdInner {
@@ -92,3 +92,21 @@ impl AsFd for TestIrqFd {
9292
unreachable!()
9393
}
9494
}
95+
96+
#[derive(Debug, Default)]
97+
pub struct TestMsiSender {
98+
pub messages: std::sync::Arc<parking_lot::Mutex<Vec<(u64, u32)>>>,
99+
}
100+
101+
impl MsiSender for TestMsiSender {
102+
type IrqFd = TestIrqFd;
103+
104+
fn send(&self, addr: u64, data: u32) -> std::result::Result<(), crate::hv::Error> {
105+
self.messages.lock().push((addr, data));
106+
Ok(())
107+
}
108+
109+
fn create_irqfd(&self) -> std::result::Result<Self::IrqFd, crate::hv::Error> {
110+
Ok(TestIrqFd::default())
111+
}
112+
}

0 commit comments

Comments
 (0)