From 046da4009e6dee30cef4216dc4b7b3b17fbc7b0f Mon Sep 17 00:00:00 2001 From: Albert Esteve Date: Tue, 29 Aug 2023 11:18:39 +0200 Subject: [PATCH] video: add tests Add test for all modules, with dev-dependencies (including rstest [1] for parametrized tests), and infrastructure. [1] - https://docs.rs/rstest/latest/rstest/ Signed-off-by: Albert Esteve --- crates/video/src/main.rs | 73 ++++++++++++++ crates/video/src/stream.rs | 88 +++++++++++++++++ crates/video/src/vhu_video.rs | 118 ++++++++++++++++++++++ crates/video/src/vhu_video_thread.rs | 127 ++++++++++++++++++++++++ crates/video/src/video.rs | 143 +++++++++++++++++++++++++++ 5 files changed, 549 insertions(+) diff --git a/crates/video/src/main.rs b/crates/video/src/main.rs index ac693d54..cc81c896 100644 --- a/crates/video/src/main.rs +++ b/crates/video/src/main.rs @@ -121,3 +121,76 @@ fn main() -> Result<()> { start_backend(VuVideoConfig::try_from(VideoArgs::parse()).unwrap()) } + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use rstest::*; + use tempfile::tempdir; + + #[rstest] + // No device specified defaults to /dev/video0 + #[case::no_device(vec!["", "-s", "video.sock", "-b", "null"], + VideoArgs { + socket_path: "video.sock".into(), + v4l2_device: "/dev/video0".into(), + backend: BackendType::Null, + })] + // Specifying device overwrite the default value + #[case::set_device(vec!["", "-s" , "video.sock", "-d", "/dev/video1", "-b", "null"], + VideoArgs { + socket_path: "video.sock".into(), + v4l2_device: "/dev/video1".into(), + backend: BackendType::Null, + })] + // Selecting different decoder + #[case::set_v4l2_decoder(vec![" ", "--socket-path", "long-video.sock", "-b", "v4l2-decoder"], + VideoArgs { + socket_path: "long-video.sock".into(), + v4l2_device: "/dev/video0".into(), + backend: BackendType::V4L2Decoder, + })] + fn test_command_line_arguments(#[case] args: Vec<&str>, #[case] command_line: VideoArgs) { + let args: VideoArgs = Parser::parse_from(args.as_slice()); + + assert_eq!( + VuVideoConfig::try_from(command_line).unwrap(), + VuVideoConfig::try_from(args).unwrap() + ); + } + + #[test] + fn test_fail_create_backend() { + use vhu_video::VuVideoError; + let config = VideoArgs { + socket_path: "video.sock".into(), + v4l2_device: "/path/invalid/video.dev".into(), + backend: BackendType::V4L2Decoder, + }; + assert_matches!( + start_backend(VuVideoConfig::try_from(config.clone()).unwrap()).unwrap_err(), + Error::CouldNotCreateBackend(VuVideoError::AccessVideoDeviceFile) + ); + } + + #[test] + fn test_fail_listener() { + use std::fs::File; + let test_dir = tempdir().expect("Could not create a temp test directory."); + let v4l2_device = test_dir.path().join("video.dev"); + File::create(&v4l2_device).expect("Could not create a test device file."); + let config = VideoArgs { + socket_path: "~/path/invalid/video.sock".into(), + v4l2_device: v4l2_device.to_owned(), + backend: BackendType::Null, + }; + assert_matches!( + start_backend(VuVideoConfig::try_from(config).unwrap()).unwrap_err(), + Error::FailedCreatingListener(_) + ); + // cleanup + std::fs::remove_file(v4l2_device).expect("Failed to clean up"); + test_dir.close().unwrap(); + } +} diff --git a/crates/video/src/stream.rs b/crates/video/src/stream.rs index b86e16cf..277c10f1 100644 --- a/crates/video/src/stream.rs +++ b/crates/video/src/stream.rs @@ -375,3 +375,91 @@ impl Stream { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::vhu_video::tests::{test_dir, VideoDeviceMock}; + use assert_matches::assert_matches; + use rstest::*; + use tempfile::TempDir; + + const TEST_PLANES: [ResourcePlane; 1] = [ResourcePlane { + offset: 0, + address: 0x100, + length: 1024, + }]; + const INVALID_MEM_TYPE: u32 = (MemoryType::VirtioObject as u32) + 1; + const INVALID_FORMAT: u32 = (Format::Fwht as u32) + 1; + + #[rstest] + fn test_video_stream(test_dir: TempDir) { + let stream_id: u32 = 1; + let v4l2_device = VideoDeviceMock::new(&test_dir); + let resource_id: u32 = 1; + let mut stream = Stream::new( + stream_id, + Path::new(&v4l2_device.path), + MemoryType::GuestPages as u32, + MemoryType::VirtioObject as u32, + Format::Fwht as u32, + ) + .expect("Failed to create stream"); + assert_matches!(stream.memory(QueueType::InputQueue), MemoryType::GuestPages); + assert_matches!( + stream.memory(QueueType::OutputQueue), + MemoryType::VirtioObject + ); + + // Add resource + let planes_layout = 0; + let res = stream.add_resource( + resource_id, + planes_layout, + Vec::from(TEST_PLANES), + QueueType::InputQueue, + ); + assert!(res.is_none()); + // Resource is retrievable + let res = stream.find_resource_mut(resource_id, QueueType::InputQueue); + assert!(res.is_some()); + let res = res.unwrap(); + assert_eq!(res.planes_layout, planes_layout); + assert_eq!(res.queue_type, QueueType::InputQueue); + assert_eq!(res.state(), ResourceState::Created); + // Change resource state + res.set_queued(); + assert!(stream.all_queued(QueueType::InputQueue)); + // Clean resources + stream.empty_resources(QueueType::InputQueue); + assert!(stream.resources_mut(QueueType::InputQueue).is_empty()); + } + + #[rstest] + #[case::invalid_in_mem( + INVALID_MEM_TYPE, MemoryType::GuestPages as u32, Format::Fwht as u32)] + #[case::invalid_out_mem( + MemoryType::VirtioObject as u32, INVALID_MEM_TYPE, Format::Nv12 as u32)] + #[case::invalid_format( + MemoryType::VirtioObject as u32, MemoryType::VirtioObject as u32, INVALID_FORMAT)] + fn test_video_stream_failures( + test_dir: TempDir, + #[case] in_mem: u32, + #[case] out_mem: u32, + #[case] format: u32, + ) { + let stream_id: u32 = 1; + let v4l2_device = VideoDeviceMock::new(&test_dir); + assert_matches!( + Stream::new( + stream_id, + Path::new(&v4l2_device.path), + in_mem, + out_mem, + format + ) + .unwrap_err(), + VuVideoError::VideoStreamCreate + ); + } +} diff --git a/crates/video/src/vhu_video.rs b/crates/video/src/vhu_video.rs index c0c01a40..c5a2da5c 100644 --- a/crates/video/src/vhu_video.rs +++ b/crates/video/src/vhu_video.rs @@ -264,3 +264,121 @@ impl VhostUserBackendMut for VuVideoBackend { self.exit_event.try_clone().ok() } } + +#[cfg(test)] +pub mod tests { + use super::*; + use rstest::*; + use std::fs::File; + use std::path::PathBuf; + use tempfile::{tempdir, TempDir}; + use vm_memory::GuestAddress; + + pub struct VideoDeviceMock { + pub path: PathBuf, + _dev: File, + } + + impl VideoDeviceMock { + pub fn new(test_dir: &TempDir) -> Self { + let v4l2_device = test_dir.path().join("video.dev"); + Self { + path: v4l2_device.to_owned(), + _dev: File::create(v4l2_device.as_path()) + .expect("Could not create a test device file."), + } + } + } + + impl Drop for VideoDeviceMock { + fn drop(&mut self) { + std::fs::remove_file(&self.path).expect("Failed to clean up test device file."); + } + } + + fn setup_backend_memory(backend: &mut VuVideoBackend) -> [VringRwLock; 2] { + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(), + ); + let vrings = [ + VringRwLock::new(mem.clone(), 0x1000).unwrap(), + VringRwLock::new(mem.clone(), 0x2000).unwrap(), + ]; + vrings[0].set_queue_info(0x100, 0x200, 0x300).unwrap(); + vrings[0].set_queue_ready(true); + vrings[1].set_queue_info(0x1100, 0x1200, 0x1300).unwrap(); + vrings[1].set_queue_ready(true); + + assert!(backend.update_memory(mem).is_ok()); + + vrings + } + + /// Creates a new test dir. There is no need to clean it after, since Drop is implemented for TempDir. + #[fixture] + pub fn test_dir() -> TempDir { + tempdir().expect("Could not create a temp test directory.") + } + + #[rstest] + fn test_video_backend(test_dir: TempDir) { + let v4l2_device = VideoDeviceMock::new(&test_dir); + let backend = VuVideoBackend::new(Path::new(&v4l2_device.path), BackendType::Null); + + assert!(backend.is_ok()); + let mut backend = backend.unwrap(); + + assert_eq!(backend.num_queues(), NUM_QUEUES); + assert_eq!(backend.max_queue_size(), QUEUE_SIZE); + assert_ne!(backend.features(), 0); + assert!(!backend.protocol_features().is_empty()); + backend.set_event_idx(false); + + let vrings = setup_backend_memory(&mut backend); + + let config = backend.get_config(0, 4); + assert_eq!(config.len(), 4); + let version = u32::from_le_bytes(config.try_into().unwrap()); + assert_eq!(version, 0); + + let exit = backend.exit_event(0); + assert!(exit.is_some()); + exit.unwrap().write(1).unwrap(); + for queue in COMMAND_Q..VIDEO_EVENT { + // Skip exit event + if queue == NUM_QUEUES as u16 { + continue; + } + let ret = backend.handle_event(queue, EventSet::IN, &vrings, 0); + assert!(ret.is_ok()); + assert!(!ret.unwrap()); + } + } + + #[rstest] + fn test_video_backend_failures(test_dir: TempDir) { + let v4l2_device = VideoDeviceMock::new(&test_dir); + let mut backend = VuVideoBackend::new(Path::new(&v4l2_device.path), BackendType::Null) + .expect("Could not create backend"); + let vrings = setup_backend_memory(&mut backend); + + // reading out of the config space, expecting empty config + let config = backend.get_config(44, 1); + assert_eq!(config.len(), 0); + + assert_eq!( + backend + .handle_event(COMMAND_Q, EventSet::OUT, &vrings, 0) + .unwrap_err() + .to_string(), + VuVideoError::HandleEventNotEpollIn.to_string() + ); + assert_eq!( + backend + .handle_event(VIDEO_EVENT + 1, EventSet::IN, &vrings, 0) + .unwrap_err() + .to_string(), + VuVideoError::HandleUnknownEvent.to_string() + ); + } +} diff --git a/crates/video/src/vhu_video_thread.rs b/crates/video/src/vhu_video_thread.rs index da0c06c6..46ce9373 100644 --- a/crates/video/src/vhu_video_thread.rs +++ b/crates/video/src/vhu_video_thread.rs @@ -570,3 +570,130 @@ impl VhostUserVideoThread { Ok(true) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::vhu_video::tests::{test_dir, VideoDeviceMock}; + use crate::vhu_video::BackendType; + use crate::video_backends::alloc_video_backend; + use assert_matches::assert_matches; + use rstest::*; + use std::path::Path; + use tempfile::TempDir; + use vm_memory::GuestAddress; + use vmm_sys_util::eventfd::EventFd; + + #[fixture] + fn dummy_fd() -> EventFd { + EventFd::new(0).expect("Could not create an EventFd.") + } + + #[rstest] + fn test_video_poller(dummy_fd: EventFd) { + let poller = VideoPoller::new().unwrap(); + assert!(poller + .add(dummy_fd.as_raw_fd(), PollerEvent::new(EventType::Write, 1)) + .is_ok()); + assert!(poller + .modify(dummy_fd.as_raw_fd(), PollerEvent::new(EventType::Read, 1)) + .is_ok()); + + // Poller captures a read event. + dummy_fd.write(1).unwrap(); + let mut epoll_events = vec![PollerEvent::default(); 1]; + let events = poller.wait(epoll_events.as_mut_slice(), 0); + assert!(events.is_ok()); + let events = events.unwrap(); + assert_eq!(events.len(), 1); + assert!(events[0].have_read); + + assert!(poller.remove(dummy_fd.as_raw_fd()).is_ok()); + // Poller captures no event, since there is no listener. + dummy_fd.write(1).unwrap(); + let events = poller.wait(epoll_events.as_mut_slice(), 0); + assert!(events.is_ok()); + let events = events.unwrap(); + assert_eq!(events.len(), 0); + } + + #[test] + fn test_video_poller_failures() { + let poller = VideoPoller::new().unwrap(); + let invalid_fd: i32 = -1; + let mut epoll_events: Vec = Vec::new(); + assert_matches!( + poller + .add(invalid_fd, PollerEvent::new(EventType::Write, 1)) + .unwrap_err(), + VuVideoError::EpollAdd(_) + ); + assert_matches!( + poller + .modify(invalid_fd, PollerEvent::new(EventType::Read, 1)) + .unwrap_err(), + VuVideoError::EpollModify(_) + ); + assert_matches!( + poller.remove(invalid_fd).unwrap_err(), + VuVideoError::EpollRemove(_) + ); + assert_matches!( + poller.wait(epoll_events.as_mut_slice(), 1).unwrap_err(), + VuVideoError::EpollWait(_) + ); + } + + #[rstest] + fn test_video_thread(test_dir: TempDir, dummy_fd: EventFd) { + let v4l2_device = VideoDeviceMock::new(&test_dir); + let backend = Arc::new(RwLock::new( + alloc_video_backend(BackendType::Null, Path::new(&v4l2_device.path)).unwrap(), + )); + let thread = VhostUserVideoThread::new(backend); + assert!(thread.is_ok()); + let mut thread = thread.unwrap(); + + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(), + ); + thread.mem = Some(mem.clone()); + + assert!(thread + .poller + .add(dummy_fd.as_raw_fd(), PollerEvent::new(EventType::Read, 1)) + .is_ok()); + + let vring = VringRwLock::new(mem, 0x1000).unwrap(); + vring.set_queue_info(0x100, 0x200, 0x300).unwrap(); + vring.set_queue_ready(true); + assert!(thread.process_command_queue(&vring).is_ok()); + + dummy_fd.write(1).unwrap(); + assert!(thread.process_video_event(&vring).is_ok()); + } + + #[rstest] + fn test_video_thread_mem_fail(test_dir: TempDir) { + assert!(alloc_video_backend(BackendType::Null, test_dir.path()).is_err()); + let v4l2_device = VideoDeviceMock::new(&test_dir); + let backend = Arc::new(RwLock::new( + alloc_video_backend(BackendType::Null, Path::new(&v4l2_device.path)).unwrap(), + )); + let thread = VhostUserVideoThread::new(backend); + assert!(thread.is_ok()); + let mut thread = thread.unwrap(); + + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(), + ); + + let vring = VringRwLock::new(mem, 0x1000).unwrap(); + + // Memory is not configured, so processing command queue should fail + assert_matches!( + thread.process_command_queue(&vring).unwrap_err(), + VuVideoError::NoMemoryConfigured + ); + } +} diff --git a/crates/video/src/video.rs b/crates/video/src/video.rs index 289ea290..a69cc70e 100644 --- a/crates/video/src/video.rs +++ b/crates/video/src/video.rs @@ -938,3 +938,146 @@ impl ToBytes for virtio_video_event { [self.event_type.as_slice(), self.stream_id.as_slice()].concat() } } + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::vhu_video::VideoDescriptorChain; + use assert_matches::assert_matches; + use rstest::*; + use virtio_queue::{mock::MockSplitQueue, Descriptor, Queue, QueueOwnedT}; + use vm_memory::{ + Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap, + }; + + fn prepare_video_desc_chain(request: &T) -> VideoDescriptorChain { + let mem = GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); + let virt_queue = MockSplitQueue::new(&mem, 16); + let addr = virt_queue.desc_table().total_size() + 0x100; + let flags = 0; + + let request = request.as_slice(); + let desc = Descriptor::new(addr, request.len() as u32, flags as u16, 1); + mem.write(request, desc.addr()).unwrap(); + assert!(virt_queue.desc_table().store(0, desc).is_ok()); + + // Put the descriptor index 0 in the first available ring position. + mem.write_obj(0u16, virt_queue.avail_addr().unchecked_add(4)) + .unwrap(); + + // Set `avail_idx` to 1. + mem.write_obj(1u16, virt_queue.avail_addr().unchecked_add(2)) + .unwrap(); + + // Create descriptor chain from pre-filled memory + virt_queue + .create_queue::() + .unwrap() + .iter(GuestMemoryAtomic::new(mem.clone()).memory()) + .unwrap() + .next() + .unwrap() + } + + #[rstest] + #[case::get_params(virtio_video_get_params { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_GET_PARAMS_EXT.into(), + stream_id: 1.into() + }, + queue_type: (QueueType::InputQueue as u32).into(), + padding: 0.into() + }, VideoCmd::GetParams { stream_id: 1, queue_type: QueueType::InputQueue })] + #[case::cap_request(virtio_video_query_capability { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_QUERY_CAPABILITY.into(), + stream_id: 1.into() + }, + queue_type: (QueueType::InputQueue as u32).into(), + padding: 0.into() + }, VideoCmd::QueryCapability { queue_type: QueueType::InputQueue })] + #[case::query_control(virtio_video_query_control { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_QUERY_CONTROL.into(), + stream_id: 1.into() + }, + control: (ControlType::Bitrate as u32).into(), + padding: 0.into(), + fmt: virtio_video_query_control_format { + format: (Format::H264 as u32).into(), + padding: 0.into(), + }, + }, VideoCmd::QueryControl { control: ControlType::Bitrate, format: Format::H264 })] + #[case::get_control(virtio_video_get_control { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_GET_CONTROL.into(), + stream_id: 1.into() + }, + control: (ControlType::ForceKeyframe as u32).into(), + padding: 0.into(), + }, VideoCmd::GetControl { stream_id: 1, control: ControlType::ForceKeyframe })] + #[case::stream_drain(virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_STREAM_DRAIN.into(), + stream_id: 1.into() + }, VideoCmd::StreamDrain { stream_id: 1 })] + #[case::stream_drain(virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_STREAM_DESTROY.into(), + stream_id: 2.into() + }, VideoCmd::StreamDestroy { stream_id: 2 })] + fn test_read_video_cmd(#[case] cmd: T, #[case] variant: VideoCmd) { + let desc_chain = prepare_video_desc_chain(&cmd); + let video_cmd = VideoCmd::from_descriptor(&desc_chain); + assert!(video_cmd.is_ok()); + assert_eq!(video_cmd.unwrap(), variant); + } + + #[rstest] + #[case::invalid_queue_type(virtio_video_get_params { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_GET_PARAMS_EXT.into(), + stream_id: 1.into() + }, + queue_type: 0.into(), + padding: 0.into() + })] + #[case::invalid_format(virtio_video_query_control { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_QUERY_CONTROL.into(), + stream_id: 1.into() + }, + control: (ControlType::Bitrate as u32).into(), + padding: 0.into(), + fmt: virtio_video_query_control_format { + format: 0.into(), + padding: 0.into(), + }, + })] + #[case::invalid_control(virtio_video_get_control { + hdr: virtio_video_cmd_hdr { + type_: VIRTIO_VIDEO_CMD_GET_CONTROL.into(), + stream_id: 1.into() + }, + control: 0.into(), + padding: 0.into(), + })] + fn test_read_video_cmd_invalid_arg(#[case] cmd: T) { + let desc_chain = prepare_video_desc_chain(&cmd); + let video_cmd = VideoCmd::from_descriptor(&desc_chain); + assert_matches!(video_cmd.unwrap_err(), VuVideoError::UnexpectedArgValue(_)); + } + + #[rstest] + #[case(0)] + #[case(VIRTIO_VIDEO_CMD_QUERY_CAPABILITY - 1)] + #[case(VIRTIO_VIDEO_CMD_GET_PARAMS__UNUSED)] + #[case(VIRTIO_VIDEO_CMD_SET_PARAMS_EXT + 1)] + fn test_read_video_cmd_invalid_cmd(#[case] invalid_type: u32) { + let cmd = virtio_video_cmd_hdr { + type_: invalid_type.into(), + stream_id: 0.into(), + }; + let desc_chain = prepare_video_desc_chain(&cmd); + let video_cmd = VideoCmd::from_descriptor(&desc_chain); + assert_matches!(video_cmd.unwrap_err(), VuVideoError::InvalidCmdType(..)); + } +}