diff --git a/Cargo.toml b/Cargo.toml index 3d2443bc..49547a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["tools/aml_tester", "tools/acpi_dumper"] +members = ["tools/aml_tester", "tools/acpi_dumper", "tools/aml_test_tools"] resolver = "2" [package] @@ -20,6 +20,10 @@ spinning_top = "0.3.0" pci_types = { version = "0.10.0", public = true } byteorder = { version = "1.5.0", default-features = false } +[dev-dependencies] +aml_test_tools = { path = "tools/aml_test_tools" } +pretty_env_logger = "0.5.0" + [features] default = ["alloc", "aml"] alloc = [] diff --git a/src/aml/mod.rs b/src/aml/mod.rs index 95736267..da23a328 100644 --- a/src/aml/mod.rs +++ b/src/aml/mod.rs @@ -95,8 +95,9 @@ impl Interpreter where H: Handler, { - /// Construct a new `Interpreter`. This does not load any tables - if you have an `AcpiTables` - /// already, use [`Interpreter::new_from_tables`] instead. + /// Construct a new [`Interpreter`]. This does not load any tables - if you have an + /// [`crate::AcpiTables`] already, construct an [`AcpiPlatform`] first and then use + /// [`Interpreter::new_from_platform`] pub fn new( handler: H, dsdt_revision: u8, @@ -120,8 +121,7 @@ where } } - /// Construct a new `Interpreter` with the given set of ACPI tables. This will automatically - /// load the DSDT and any SSDTs in the supplied [`AcpiTables`]. + /// Construct a new [`Interpreter`] with the given [`AcpiPlatform`]. pub fn new_from_platform(platform: &AcpiPlatform) -> Result, AcpiError> { fn load_table(interpreter: &Interpreter, table: AmlTable) -> Result<(), AcpiError> { let mapping = unsafe { @@ -158,7 +158,7 @@ where } /// Load the supplied byte stream as an AML table. This should be only the encoded AML stream - - /// not the header at the start of a table. If you've used [`Interpreter::new_from_tables`], + /// not the header at the start of a table. If you've used [`Interpreter::new_from_platform`], /// you'll likely not need to load any tables manually. pub fn load_table(&self, stream: &[u8]) -> Result<(), AmlError> { let context = unsafe { MethodContext::new_from_table(stream) }; diff --git a/src/lib.rs b/src/lib.rs index 0030e8c6..03e4c290 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! //! Next, you'll need to get the physical address of either the RSDP, or the RSDT/XSDT. The method //! for doing this depends on the platform you're running on and how you were booted. If you know -//! the system was booted via the BIOS, you can use [`rsdp::Rsdp::search_for_on_bios`]. UEFI provides a +//! the system was booted via the BIOS, you can use [`Rsdp::search_for_on_bios`]. UEFI provides a //! separate mechanism for getting the address of the RSDP. //! //! You then need to construct an instance of [`AcpiTables`], which can be done in a few ways @@ -29,7 +29,18 @@ //! * Use [`AcpiTables::from_rsdt`] if you have the physical address of the RSDT/XSDT //! //! Once you have an [`AcpiTables`], you can search for relevant tables, or use the higher-level -//! interfaces, such as [`PlatformInfo`], [`PciConfigRegions`], or [`HpetInfo`]. +//! interfaces, such as [`PowerProfile`], or [`HpetInfo`]. +//! +//! If you have the `aml` feature enabled then you can construct an +//! [AML interpreter](`crate::aml::Interpreter`) by first constructing an +//! [`AcpiPlatform`](platform::AcpiPlatform) and then the interpreter: +//! +//! ```ignore,rust +//! let mut acpi_handler = YourHandler::new(/*args*/); +//! let acpi_tables = unsafe { AcpiTables::from_rsdp(acpi_handler.clone(), rsdp_addr) }.unwrap(); +//! let platform = AcpiPlatform::new(acpi_tables, acpi_handler).unwrap(); +//! let interpreter = Interpreter::new_from_platform(&platform).unwrap(); +//! ``` #![no_std] #![feature(allocator_api)] @@ -398,9 +409,9 @@ pub trait Handler: Clone { /// - `size` must be at least `size_of::()`. unsafe fn map_physical_region(&self, physical_address: usize, size: usize) -> PhysicalMapping; - /// Unmap the given physical mapping. This is called when a `PhysicalMapping` is dropped, you should **not** manually call this. + /// Unmap the given physical mapping. This is called when a [`PhysicalMapping`] is dropped, you should **not** manually call this. /// - /// Note: A reference to the `Handler` used to construct `region` can be acquired by calling [`PhysicalMapping::mapper`]. + /// Note: A reference to the [`Handler`] used to construct `region` can be acquired from [`PhysicalMapping::handler`]. fn unmap_physical_region(region: &PhysicalMapping); // TODO: maybe we should map stuff ourselves in the AML interpreter and do this internally? diff --git a/tests/normal_fields.rs b/tests/normal_fields.rs new file mode 100644 index 00000000..e1a88754 --- /dev/null +++ b/tests/normal_fields.rs @@ -0,0 +1,109 @@ +// Test operations on "normal" fields - those that are not Index or Bank fields. + +use aml_test_tools::handlers::std_test_handler::{ + Command, + construct_std_handler, + create_mutex, + read_io_u8, + read_io_u16, + read_u16, + write_io_u8, + write_io_u16, + write_u16, +}; + +mod test_infra; + +#[test] +fn test_basic_store_and_load() { + const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) { + OperationRegion(MEM, SystemMemory, 0x40000, 0x1000) + Field(MEM, WordAcc, NoLock, Preserve) { + A, 16, + B, 16 + } + + Method(MAIN, 0, NotSerialized) { + A = 0xA5A5 + B = A + Return (0) + } +} +"#; + + const EXPECTED_COMMANDS: &[Command] = &[ + create_mutex(), + // A = 0xA5A5 + write_u16(0x40000, 0xA5A5), + // B = A + read_u16(0x40000, 0xA5A5), + write_u16(0x40002, 0xA5A5), + ]; + + let handler = construct_std_handler(EXPECTED_COMMANDS.to_vec()); + test_infra::run_aml_test(AML, handler); +} + +#[test] +fn test_narrow_access_store_and_load() { + const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) { + OperationRegion(MEM, SystemIO, 0x40, 0x10) + Field(MEM, ByteAcc, NoLock, Preserve) { + A, 16, + B, 16 + } + + Method(MAIN, 0, NotSerialized) { + A = 0xA55A + B = A + Return (0) + } +} +"#; + + const EXPECTED_COMMANDS: &[Command] = &[ + create_mutex(), + // A = 0xA55A + write_io_u8(0x40, 0x5A), + write_io_u8(0x41, 0xA5), + // B = A + read_io_u8(0x40, 0x5A), + read_io_u8(0x41, 0xA5), + write_io_u8(0x42, 0x5A), + write_io_u8(0x43, 0xA5), + ]; + + let handler = construct_std_handler(EXPECTED_COMMANDS.to_vec()); + test_infra::run_aml_test(AML, handler); +} + +#[test] +fn test_unaligned_field_store() { + const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) { + OperationRegion(MEM, SystemIO, 0x40, 0x10) + Field(MEM, WordAcc, NoLock, Preserve) { + A, 7, + B, 8 + } + + Method(MAIN, 0, NotSerialized) { + A = 4 + B = A + + Return (0) + } +} +"#; + + const EXPECTED_COMMANDS: &[Command] = &[ + create_mutex(), + read_io_u16(0x40, 0), + write_io_u16(0x40, 0x04), + read_io_u16(0x40, 4), + read_io_u16(0x40, 4), + write_io_u16(0x40, 0x204), + ]; + + let handler = construct_std_handler(EXPECTED_COMMANDS.to_vec()); + test_infra::run_aml_test(AML, handler); +} diff --git a/tests/test_infra/mod.rs b/tests/test_infra/mod.rs new file mode 100644 index 00000000..31c5ff88 --- /dev/null +++ b/tests/test_infra/mod.rs @@ -0,0 +1,13 @@ +use acpi::Handler; +use aml_test_tools::{new_interpreter, run_test_for_string, TestResult}; +use aml_test_tools::handlers::logging_handler::LoggingHandler; + +pub fn run_aml_test(asl: &'static str, handler: impl Handler) { + // Tests calling `run_aml_test` don't do much else, and we usually want logging, so initialize it here. + let _ = pretty_env_logger::try_init(); + + let logged_handler = LoggingHandler::new(handler); + let mut interpreter = new_interpreter(logged_handler); + + assert_eq!(run_test_for_string(asl, &mut interpreter), TestResult::Pass); +} diff --git a/tools/aml_test_tools/Cargo.toml b/tools/aml_test_tools/Cargo.toml new file mode 100644 index 00000000..73cd8c08 --- /dev/null +++ b/tools/aml_test_tools/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aml_test_tools" +version = "0.1.0" +authors = ["Isaac Woods", "Martin Hughes"] +edition = "2024" +publish = false + +[dependencies] +acpi = { path = "../.." } +log = "0.4" +pci_types = "0.10.0" +tempfile = "3.26.0" diff --git a/tools/aml_test_tools/src/handlers/check_cmd_handler.rs b/tools/aml_test_tools/src/handlers/check_cmd_handler.rs new file mode 100644 index 00000000..bb5c81f5 --- /dev/null +++ b/tools/aml_test_tools/src/handlers/check_cmd_handler.rs @@ -0,0 +1,325 @@ +//! A wrapper around another [`Handler`] that checks for the correct sequence of commands in a test. + +use acpi::{Handle, Handler, PhysicalMapping, aml::AmlError}; +use pci_types::PciAddress; +use std::{ + mem::ManuallyDrop, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering::Relaxed}, + }, +}; + +/// The commands that may be received by an ACPI [`handler`](Handler). +/// +/// They are written as an enum to allow a list of commands to be stored in a [`Vec`] or similar. +/// A `Vec` of these is used by [`CheckCommandHandler`] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AcpiCommands { + MapPhysicalRegion(usize, usize), + UnmapPhysicalRegion(usize), + ReadU8(usize), + ReadU16(usize), + ReadU32(usize), + ReadU64(usize), + WriteU8(usize, u8), + WriteU16(usize, u16), + WriteU32(usize, u32), + WriteU64(usize, u64), + ReadIoU8(u16), + ReadIoU16(u16), + ReadIoU32(u16), + WriteIoU8(u16, u8), + WriteIoU16(u16, u16), + WriteIoU32(u16, u32), + ReadPciU8(PciAddress, u16), + ReadPciU16(PciAddress, u16), + ReadPciU32(PciAddress, u16), + WritePciU8(PciAddress, u16, u8), + WritePciU16(PciAddress, u16, u16), + WritePciU32(PciAddress, u16, u32), + NanosSinceBoot, + Stall(u64), + Sleep(u64), + CreateMutex, + Acquire(Handle, u16), + Release(Handle), +} + +/// A wrapper around another [`Handler`] that checks the correct sequence of commands are being +/// generated, and that they have the expected parameters. +#[derive(Clone, Debug)] +pub struct CheckCommandHandler +where + H: Handler + Clone, +{ + commands: Vec, + next_command_idx: Arc, + next_handler: H, +} + +impl CheckCommandHandler +where + H: Handler + Clone, +{ + pub fn new(commands: Vec, next_handler: H) -> Self { + Self { commands, next_command_idx: Arc::new(AtomicUsize::new(0)), next_handler } + } + + fn check_command(&self, command: AcpiCommands) { + let next_command_idx = self.next_command_idx.fetch_add(1, Relaxed); + let next_command = self.commands.get(next_command_idx); + + if next_command.is_none() { + panic!("More commands attempted than expected"); + }; + + assert_eq!(*next_command.unwrap(), command); + } +} + +impl Drop for CheckCommandHandler +where + H: Handler + Clone, +{ + fn drop(&mut self) { + // Don't do this if the test has already failed, to avoid a double-panic. + if !std::thread::panicking() { + assert_eq!( + self.next_command_idx.load(std::sync::atomic::Ordering::Relaxed), + self.commands.len(), + "Not all commands were executed" + ); + } + } +} + +impl Handler for CheckCommandHandler +where + H: Handler + Clone, +{ + unsafe fn map_physical_region(&self, physical_address: usize, size: usize) -> PhysicalMapping { + self.check_command(AcpiCommands::MapPhysicalRegion(physical_address, size)); + + let inner_mapping = unsafe { self.next_handler.map_physical_region::(physical_address, size) }; + let inner_mapping = ManuallyDrop::new(inner_mapping); + + PhysicalMapping { + physical_start: inner_mapping.physical_start, + virtual_start: inner_mapping.virtual_start, + region_length: inner_mapping.region_length, + mapped_length: inner_mapping.mapped_length, + handler: self.clone(), + } + } + + fn unmap_physical_region(region: &PhysicalMapping) { + // This function can be called during a panic, and it's pretty unlikely this command will + // be in the expected commands list... + // + // Also stop checking if we're at or past the end of the command list. This stops any + // confusion about whether we're in Drop or not. + if !std::thread::panicking() + && region.handler.commands.len() > region.handler.next_command_idx.load(Relaxed) + { + region.handler.check_command(AcpiCommands::UnmapPhysicalRegion(region.physical_start)); + } + + // Convert `PhysicalMapping, T>` -> `PhysicalMapping` and delegate. + // Prevent the temporary mapping from being dropped (and thus calling `H::unmap_physical_region` twice). + let inner_region = ManuallyDrop::new(PhysicalMapping:: { + physical_start: region.physical_start, + virtual_start: region.virtual_start, + region_length: region.region_length, + mapped_length: region.mapped_length, + handler: region.handler.next_handler.clone(), + }); + + H::unmap_physical_region(&inner_region); + } + + fn read_u8(&self, address: usize) -> u8 { + self.check_command(AcpiCommands::ReadU8(address)); + self.next_handler.read_u8(address) + } + + fn read_u16(&self, address: usize) -> u16 { + self.check_command(AcpiCommands::ReadU16(address)); + self.next_handler.read_u16(address) + } + + fn read_u32(&self, address: usize) -> u32 { + self.check_command(AcpiCommands::ReadU32(address)); + self.next_handler.read_u32(address) + } + + fn read_u64(&self, address: usize) -> u64 { + self.check_command(AcpiCommands::ReadU64(address)); + self.next_handler.read_u64(address) + } + + fn write_u8(&self, address: usize, value: u8) { + self.check_command(AcpiCommands::WriteU8(address, value)); + self.next_handler.write_u8(address, value); + } + + fn write_u16(&self, address: usize, value: u16) { + self.check_command(AcpiCommands::WriteU16(address, value)); + self.next_handler.write_u16(address, value); + } + + fn write_u32(&self, address: usize, value: u32) { + self.check_command(AcpiCommands::WriteU32(address, value)); + self.next_handler.write_u32(address, value); + } + + fn write_u64(&self, address: usize, value: u64) { + self.check_command(AcpiCommands::WriteU64(address, value)); + self.next_handler.write_u64(address, value); + } + + fn read_io_u8(&self, port: u16) -> u8 { + self.check_command(AcpiCommands::ReadIoU8(port)); + self.next_handler.read_io_u8(port) + } + + fn read_io_u16(&self, port: u16) -> u16 { + self.check_command(AcpiCommands::ReadIoU16(port)); + self.next_handler.read_io_u16(port) + } + + fn read_io_u32(&self, port: u16) -> u32 { + self.check_command(AcpiCommands::ReadIoU32(port)); + self.next_handler.read_io_u32(port) + } + + fn write_io_u8(&self, port: u16, value: u8) { + self.check_command(AcpiCommands::WriteIoU8(port, value)); + self.next_handler.write_io_u8(port, value); + } + + fn write_io_u16(&self, port: u16, value: u16) { + self.check_command(AcpiCommands::WriteIoU16(port, value)); + self.next_handler.write_io_u16(port, value); + } + + fn write_io_u32(&self, port: u16, value: u32) { + self.check_command(AcpiCommands::WriteIoU32(port, value)); + self.next_handler.write_io_u32(port, value); + } + + fn read_pci_u8(&self, address: PciAddress, offset: u16) -> u8 { + self.check_command(AcpiCommands::ReadPciU8(address, offset)); + self.next_handler.read_pci_u8(address, offset) + } + + fn read_pci_u16(&self, address: PciAddress, offset: u16) -> u16 { + self.check_command(AcpiCommands::ReadPciU16(address, offset)); + self.next_handler.read_pci_u16(address, offset) + } + + fn read_pci_u32(&self, address: PciAddress, offset: u16) -> u32 { + self.check_command(AcpiCommands::ReadPciU32(address, offset)); + self.next_handler.read_pci_u32(address, offset) + } + + fn write_pci_u8(&self, address: PciAddress, offset: u16, value: u8) { + self.check_command(AcpiCommands::WritePciU8(address, offset, value)); + self.next_handler.write_pci_u8(address, offset, value); + } + + fn write_pci_u16(&self, address: PciAddress, offset: u16, value: u16) { + self.check_command(AcpiCommands::WritePciU16(address, offset, value)); + self.next_handler.write_pci_u16(address, offset, value); + } + + fn write_pci_u32(&self, address: PciAddress, offset: u16, value: u32) { + self.check_command(AcpiCommands::WritePciU32(address, offset, value)); + self.next_handler.write_pci_u32(address, offset, value); + } + + fn nanos_since_boot(&self) -> u64 { + self.check_command(AcpiCommands::NanosSinceBoot); + self.next_handler.nanos_since_boot() + } + + fn stall(&self, microseconds: u64) { + self.check_command(AcpiCommands::Stall(microseconds)); + self.next_handler.stall(microseconds); + } + + fn sleep(&self, milliseconds: u64) { + self.check_command(AcpiCommands::Sleep(milliseconds)); + self.next_handler.sleep(milliseconds); + } + + fn create_mutex(&self) -> Handle { + self.check_command(AcpiCommands::CreateMutex); + self.next_handler.create_mutex() + } + + fn acquire(&self, mutex: Handle, timeout: u16) -> Result<(), AmlError> { + self.check_command(AcpiCommands::Acquire(mutex, timeout)); + self.next_handler.acquire(mutex, timeout) + } + + fn release(&self, mutex: Handle) { + self.check_command(AcpiCommands::Release(mutex)); + self.next_handler.release(mutex); + } +} + +#[cfg(test)] +mod test { + use crate::handlers::null_handler::NullHandler; + use super::*; + + #[test] + fn handler_basic_functions() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::WriteIoU16(3, 4)]; + + let handler = CheckCommandHandler::new(test_commands, NullHandler {}); + handler.read_io_u8(2); + handler.write_io_u16(3, 4); + } + + #[test] + #[should_panic] + fn handler_fails_for_wrong_command() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::WriteIoU16(3, 4)]; + + let handler = CheckCommandHandler::new(test_commands, NullHandler {}); + handler.read_io_u8(3); + // We shouldn't actually make it to this command, but it makes sure the handler doesn't panic for having too few + // commands sent to it. + handler.write_io_u16(3, 4); + } + + #[test] + #[should_panic] + fn handler_fails_for_too_many_commands() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::WriteIoU16(3, 4)]; + + let handler = CheckCommandHandler::new(test_commands, NullHandler {}); + handler.read_io_u8(2); + handler.write_io_u16(3, 4); + handler.read_io_u8(5); + } + + #[test] + #[should_panic] + fn handler_fails_for_too_few_commands() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::WriteIoU16(3, 4)]; + + let handler = CheckCommandHandler::new(test_commands, NullHandler {}); + handler.read_io_u8(2); + } + + #[test] + #[should_panic] + fn check_handler_fails_gracefully_for_no_commands() { + let test_commands: Vec = vec![]; + let handler = CheckCommandHandler::new(test_commands, NullHandler {}); + handler.read_io_u8(2); + } +} \ No newline at end of file diff --git a/tools/aml_test_tools/src/handlers/listed_response_handler.rs b/tools/aml_test_tools/src/handlers/listed_response_handler.rs new file mode 100644 index 00000000..9711afcf --- /dev/null +++ b/tools/aml_test_tools/src/handlers/listed_response_handler.rs @@ -0,0 +1,254 @@ +//! A basic [`Handler`] that returns an expected result from a provided sequence of commands. + +use acpi::{Handle, Handler, PhysicalMapping, aml::AmlError}; +use pci_types::PciAddress; +use std::sync::{Arc, atomic::AtomicUsize}; + +/// Commands that may be received by a [handler](Handler) which return a value from the handler. +/// +/// Commands that do not return a value are represented by [`Skip`](AcpiCommands::Skip). +/// +/// A [`Vec`] of these is used by [`ListedResponseHandler`] +// Some variants are unused for the time being, until more tests are written. +#[allow(unused)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AcpiCommands { + /// A stand-in for all commands that don't return a value. + Skip(), + ReadU8(u8), + ReadU16(u16), + ReadU32(u32), + ReadU64(u64), + ReadIoU8(u8), + ReadIoU16(u16), + ReadIoU32(u32), + ReadPciU8(u8), + ReadPciU16(u16), + ReadPciU32(u32), + NanosSinceBoot(u64), +} + +/// A basic [`Handler`] that returns an expected result from a provided sequence of commands. +/// +/// If the command is unexpected, this handler will panic. +#[derive(Clone, Debug)] +pub struct ListedResponseHandler { + commands: Vec, + next_command_idx: Arc, +} + +impl ListedResponseHandler { + pub fn new(commands: Vec) -> Self { + Self { commands, next_command_idx: Arc::new(AtomicUsize::new(0)) } + } + + fn get_next_command(&self) -> AcpiCommands { + let next_command_idx = self.next_command_idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let next_command = self.commands.get(next_command_idx); + + if next_command.is_none() { + panic!("More commands attempted than expected"); + }; + + *next_command.unwrap() + } +} + +macro_rules! check_and_get_result { + ($self:ident, $expected_cmd:ident) => { + match $self.get_next_command() { + AcpiCommands::$expected_cmd(x) => x, + _ => panic!("Unexpected command"), + } + }; +} + +macro_rules! check_is_skipped { + ($self:ident) => { + match $self.get_next_command() { + AcpiCommands::Skip() => (), + _ => panic!("Unexpected command"), + } + }; +} +impl Drop for ListedResponseHandler { + fn drop(&mut self) { + // Don't do this if the test has already failed, to avoid a double-panic. + if !std::thread::panicking() { + assert_eq!( + self.next_command_idx.load(std::sync::atomic::Ordering::Relaxed), + self.commands.len(), + "Not all commands were executed" + ); + } + } +} + +impl Handler for ListedResponseHandler { + unsafe fn map_physical_region(&self, _physical_address: usize, _size: usize) -> PhysicalMapping { + // This isn't implemented in `aml_tester` either + todo!() + } + + fn unmap_physical_region(_region: &PhysicalMapping) {} + + fn read_u8(&self, _address: usize) -> u8 { + check_and_get_result!(self, ReadU8) + } + + fn read_u16(&self, _address: usize) -> u16 { + check_and_get_result!(self, ReadU16) + } + + fn read_u32(&self, _address: usize) -> u32 { + check_and_get_result!(self, ReadU32) + } + + fn read_u64(&self, _address: usize) -> u64 { + check_and_get_result!(self, ReadU64) + } + + fn write_u8(&self, _address: usize, _value: u8) { + check_is_skipped!(self) + } + + fn write_u16(&self, _address: usize, _value: u16) { + check_is_skipped!(self) + } + + fn write_u32(&self, _address: usize, _value: u32) { + check_is_skipped!(self) + } + + fn write_u64(&self, _address: usize, _value: u64) { + check_is_skipped!(self) + } + + fn read_io_u8(&self, _port: u16) -> u8 { + check_and_get_result!(self, ReadIoU8) + } + + fn read_io_u16(&self, _port: u16) -> u16 { + check_and_get_result!(self, ReadIoU16) + } + + fn read_io_u32(&self, _port: u16) -> u32 { + check_and_get_result!(self, ReadIoU32) + } + + fn write_io_u8(&self, _port: u16, _value: u8) { + check_is_skipped!(self) + } + + fn write_io_u16(&self, _port: u16, _value: u16) { + check_is_skipped!(self) + } + + fn write_io_u32(&self, _port: u16, _value: u32) { + check_is_skipped!(self) + } + + fn read_pci_u8(&self, _address: PciAddress, _offset: u16) -> u8 { + check_and_get_result!(self, ReadPciU8) + } + + fn read_pci_u16(&self, _address: PciAddress, _offset: u16) -> u16 { + check_and_get_result!(self, ReadPciU16) + } + + fn read_pci_u32(&self, _address: PciAddress, _offset: u16) -> u32 { + check_and_get_result!(self, ReadPciU32) + } + + fn write_pci_u8(&self, _address: PciAddress, _offset: u16, _value: u8) { + check_is_skipped!(self) + } + + fn write_pci_u16(&self, _address: PciAddress, _offset: u16, _value: u16) { + check_is_skipped!(self) + } + + fn write_pci_u32(&self, _address: PciAddress, _offset: u16, _value: u32) { + check_is_skipped!(self) + } + + fn nanos_since_boot(&self) -> u64 { + check_and_get_result!(self, NanosSinceBoot) + } + + fn stall(&self, _microseconds: u64) { + check_is_skipped!(self) + } + + fn sleep(&self, _milliseconds: u64) { + check_is_skipped!(self) + } + + fn create_mutex(&self) -> Handle { + check_is_skipped!(self); + Handle(1) + } + + fn acquire(&self, _mutex: Handle, _timeout: u16) -> Result<(), AmlError> { + check_is_skipped!(self); + Ok(()) + } + + fn release(&self, _mutex: Handle) { + check_is_skipped!(self) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn handler_basic_functions() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::Skip()]; + + let handler = ListedResponseHandler::new(test_commands); + handler.read_io_u8(2); + handler.write_io_u16(3, 4); + } + + #[test] + #[should_panic] + fn handler_fails_for_wrong_command() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::ReadIoU8(3)]; + + let handler = ListedResponseHandler::new(test_commands); + handler.read_io_u8(3); + // We shouldn't actually make it to this command, but it makes sure the handler doesn't panic for having too few + // commands sent to it. + handler.write_io_u16(3, 4); + } + + #[test] + #[should_panic] + fn handler_fails_for_too_many_commands() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::Skip()]; + + let handler = ListedResponseHandler::new(test_commands); + handler.read_io_u8(2); + handler.write_io_u16(3, 4); + handler.read_io_u8(5); + } + + #[test] + #[should_panic] + fn handler_fails_for_too_few_commands() { + let test_commands = vec![AcpiCommands::ReadIoU8(2), AcpiCommands::Skip()]; + + let handler = ListedResponseHandler::new(test_commands); + handler.read_io_u8(2); + } + + #[test] + #[should_panic] + fn check_handler_fails_gracefully_for_no_commands() { + let test_commands: Vec = vec![]; + let handler = ListedResponseHandler::new(test_commands); + handler.read_io_u8(2); + } +} diff --git a/tools/aml_test_tools/src/handlers/logging_handler.rs b/tools/aml_test_tools/src/handlers/logging_handler.rs new file mode 100644 index 00000000..80049e45 --- /dev/null +++ b/tools/aml_test_tools/src/handlers/logging_handler.rs @@ -0,0 +1,204 @@ +//! A [`Handler`] that logs all calls, then forwards them to an inner handler. + +use acpi::{Handle, Handler, PhysicalMapping, aml::object::Object}; +use core::mem::ManuallyDrop; +use log::info; +use pci_types::PciAddress; + +/// A [`Handler`] wrapper that logs every call to `info!` and then forwards it to an inner handler. +/// +/// Use this Handler to have a logging style consistent with the [`acpi`] crate's tests. +#[derive(Clone)] +pub struct LoggingHandler { + next_handler: H, +} + +impl LoggingHandler +where + H: Handler, +{ + pub fn new(next_handler: H) -> Self { + Self { next_handler } + } +} + +impl Handler for LoggingHandler +where + H: Handler, +{ + unsafe fn map_physical_region(&self, physical_address: usize, size: usize) -> PhysicalMapping { + info!("map_physical_region(physical_address={:#x}, size={:#x})", physical_address, size); + + let inner_mapping = unsafe { self.next_handler.map_physical_region::(physical_address, size) }; + let inner_mapping = ManuallyDrop::new(inner_mapping); + + PhysicalMapping { + physical_start: inner_mapping.physical_start, + virtual_start: inner_mapping.virtual_start, + region_length: inner_mapping.region_length, + mapped_length: inner_mapping.mapped_length, + handler: self.clone(), + } + } + + fn unmap_physical_region(region: &PhysicalMapping) { + info!("unmap_physical_region(physical_start={:#x})", region.physical_start); + + // Convert `PhysicalMapping, T>` -> `PhysicalMapping` and delegate. + // Prevent the temporary mapping from being dropped (and thus calling `H::unmap_physical_region` twice). + let inner_region = ManuallyDrop::new(PhysicalMapping:: { + physical_start: region.physical_start, + virtual_start: region.virtual_start, + region_length: region.region_length, + mapped_length: region.mapped_length, + handler: region.handler.next_handler.clone(), + }); + + H::unmap_physical_region(&inner_region); + } + + fn read_u8(&self, address: usize) -> u8 { + let value = self.next_handler.read_u8(address); + info!("read_u8(address={:#x}) -> {:#x}", address, value); + value + } + + fn read_u16(&self, address: usize) -> u16 { + let value = self.next_handler.read_u16(address); + info!("read_u16(address={:#x}) -> {:#x}", address, value); + value + } + + fn read_u32(&self, address: usize) -> u32 { + let value = self.next_handler.read_u32(address); + info!("read_u32(address={:#x}) -> {:#x}", address, value); + value + } + + fn read_u64(&self, address: usize) -> u64 { + let value = self.next_handler.read_u64(address); + info!("read_u64(address={:#x}) -> {:#x}", address, value); + value + } + + fn write_u8(&self, address: usize, value: u8) { + info!("write_u8(address={:#x}, value={:#x})", address, value); + self.next_handler.write_u8(address, value); + } + + fn write_u16(&self, address: usize, value: u16) { + info!("write_u16(address={:#x}, value={:#x})", address, value); + self.next_handler.write_u16(address, value); + } + + fn write_u32(&self, address: usize, value: u32) { + info!("write_u32(address={:#x}, value={:#x})", address, value); + self.next_handler.write_u32(address, value); + } + + fn write_u64(&self, address: usize, value: u64) { + info!("write_u64(address={:#x}, value={:#x})", address, value); + self.next_handler.write_u64(address, value); + } + + fn read_io_u8(&self, port: u16) -> u8 { + let value = self.next_handler.read_io_u8(port); + info!("read_io_u8(port={:#x}) -> {:#x}", port, value); + value + } + + fn read_io_u16(&self, port: u16) -> u16 { + let value = self.next_handler.read_io_u16(port); + info!("read_io_u16(port={:#x}) -> {:#x}", port, value); + value + } + + fn read_io_u32(&self, port: u16) -> u32 { + let value = self.next_handler.read_io_u32(port); + info!("read_io_u32(port={:#x}) -> {:#x}", port, value); + value + } + + fn write_io_u8(&self, port: u16, value: u8) { + info!("write_io_u8(port={:#x}, value={:#x})", port, value); + self.next_handler.write_io_u8(port, value); + } + + fn write_io_u16(&self, port: u16, value: u16) { + info!("write_io_u16(port={:#x}, value={:#x})", port, value); + self.next_handler.write_io_u16(port, value); + } + + fn write_io_u32(&self, port: u16, value: u32) { + info!("write_io_u32(port={:#x}, value={:#x})", port, value); + self.next_handler.write_io_u32(port, value); + } + + fn read_pci_u8(&self, address: PciAddress, offset: u16) -> u8 { + let value = self.next_handler.read_pci_u8(address, offset); + info!("read_pci_u8(address={:?}, offset={:#x}) -> {:#x}", address, offset, value); + value + } + + fn read_pci_u16(&self, address: PciAddress, offset: u16) -> u16 { + let value = self.next_handler.read_pci_u16(address, offset); + info!("read_pci_u16(address={:?}, offset={:#x}) -> {:#x}", address, offset, value); + value + } + + fn read_pci_u32(&self, address: PciAddress, offset: u16) -> u32 { + let value = self.next_handler.read_pci_u32(address, offset); + info!("read_pci_u32(address={:?}, offset={:#x}) -> {:#x}", address, offset, value); + value + } + + fn write_pci_u8(&self, address: PciAddress, offset: u16, value: u8) { + info!("write_pci_u8(address={:?}, offset={:#x}, value={:#x})", address, offset, value); + self.next_handler.write_pci_u8(address, offset, value); + } + + fn write_pci_u16(&self, address: PciAddress, offset: u16, value: u16) { + info!("write_pci_u16(address={:?}, offset={:#x}, value={:#x})", address, offset, value); + self.next_handler.write_pci_u16(address, offset, value); + } + + fn write_pci_u32(&self, address: PciAddress, offset: u16, value: u32) { + info!("write_pci_u32(address={:?}, offset={:#x}, value={:#x})", address, offset, value); + self.next_handler.write_pci_u32(address, offset, value); + } + + fn nanos_since_boot(&self) -> u64 { + info!("nanos_since_boot()"); + self.next_handler.nanos_since_boot() + } + + fn stall(&self, microseconds: u64) { + info!("stall(microseconds={})", microseconds); + self.next_handler.stall(microseconds); + } + + fn sleep(&self, milliseconds: u64) { + info!("sleep(milliseconds={})", milliseconds); + self.next_handler.sleep(milliseconds); + } + + fn create_mutex(&self) -> Handle { + info!("create_mutex()"); + self.next_handler.create_mutex() + } + + fn acquire(&self, mutex: Handle, timeout: u16) -> Result<(), acpi::aml::AmlError> { + info!("acquire(mutex={:?}, timeout={})", mutex, timeout); + self.next_handler.acquire(mutex, timeout) + } + + fn release(&self, mutex: Handle) { + info!("release(mutex={:?})", mutex); + self.next_handler.release(mutex); + } + + fn handle_debug(&self, object: &Object) { + info!("Debug store: {}", object); + self.next_handler.handle_debug(object); + } +} diff --git a/tools/aml_test_tools/src/handlers/mod.rs b/tools/aml_test_tools/src/handlers/mod.rs new file mode 100644 index 00000000..5a6ed001 --- /dev/null +++ b/tools/aml_test_tools/src/handlers/mod.rs @@ -0,0 +1,9 @@ +//! A collection of [`handlers`](acpi::Handler) that might be useful when testing AML. +//! +//! These are all used by the [`acpi`] integration tests, or by `aml_tester`. + +pub mod check_cmd_handler; +pub mod listed_response_handler; +pub mod logging_handler; +pub mod null_handler; +pub mod std_test_handler; diff --git a/tools/aml_test_tools/src/handlers/null_handler.rs b/tools/aml_test_tools/src/handlers/null_handler.rs new file mode 100644 index 00000000..0e6048c2 --- /dev/null +++ b/tools/aml_test_tools/src/handlers/null_handler.rs @@ -0,0 +1,98 @@ +//! A [`Handler`] that does nothing useful. + +use acpi::{Handle, Handler, PhysicalMapping, aml::AmlError}; +use pci_types::PciAddress; + +#[derive(Clone)] +pub struct NullHandler; + +/// A [`Handler`] that does nothing. If required, it will generally return zero. +/// +/// This is useful as a placeholder if you really don't care what values the core [`acpi`] parser +/// receives. +impl Handler for NullHandler { + unsafe fn map_physical_region(&self, _physical_address: usize, _size: usize) -> PhysicalMapping { + // This isn't implemented in `aml_tester` either + todo!() + } + + fn unmap_physical_region(_region: &PhysicalMapping) {} + + fn read_u8(&self, _address: usize) -> u8 { + 0 + } + + fn read_u16(&self, _address: usize) -> u16 { + 0 + } + + fn read_u32(&self, _address: usize) -> u32 { + 0 + } + + fn read_u64(&self, _address: usize) -> u64 { + 0 + } + + fn write_u8(&self, _address: usize, _value: u8) {} + + fn write_u16(&self, _address: usize, _value: u16) {} + + fn write_u32(&self, _address: usize, _value: u32) {} + + fn write_u64(&self, _address: usize, _value: u64) {} + + fn read_io_u8(&self, _port: u16) -> u8 { + 0 + } + + fn read_io_u16(&self, _port: u16) -> u16 { + 0 + } + + fn read_io_u32(&self, _port: u16) -> u32 { + 0 + } + + fn write_io_u8(&self, _port: u16, _value: u8) {} + + fn write_io_u16(&self, _port: u16, _value: u16) {} + + fn write_io_u32(&self, _port: u16, _value: u32) {} + + fn read_pci_u8(&self, _address: PciAddress, _offset: u16) -> u8 { + 0 + } + + fn read_pci_u16(&self, _address: PciAddress, _offset: u16) -> u16 { + 0 + } + + fn read_pci_u32(&self, _address: PciAddress, _offset: u16) -> u32 { + 0 + } + + fn write_pci_u8(&self, _address: PciAddress, _offset: u16, _value: u8) {} + + fn write_pci_u16(&self, _address: PciAddress, _offset: u16, _value: u16) {} + + fn write_pci_u32(&self, _address: PciAddress, _offset: u16, _value: u32) {} + + fn nanos_since_boot(&self) -> u64 { + 1000 + } + + fn stall(&self, _microseconds: u64) {} + + fn sleep(&self, _milliseconds: u64) {} + + fn create_mutex(&self) -> Handle { + Handle(0) + } + + fn acquire(&self, _mutex: Handle, _timeout: u16) -> Result<(), AmlError> { + Ok(()) + } + + fn release(&self, _mutex: Handle) {} +} diff --git a/tools/aml_test_tools/src/handlers/std_test_handler.rs b/tools/aml_test_tools/src/handlers/std_test_handler.rs new file mode 100644 index 00000000..cfbdd64d --- /dev/null +++ b/tools/aml_test_tools/src/handlers/std_test_handler.rs @@ -0,0 +1,165 @@ +//! Rather than defining a [`Handler`], this module defines useful functions to streamline the most- +//! used case of a [`CheckCommandHandler`] wrapping a [`ListedResponseHandler`]. + +use pci_types::PciAddress; +use crate::handlers::{ + check_cmd_handler::{AcpiCommands as Check, CheckCommandHandler}, + listed_response_handler::{AcpiCommands as Response, ListedResponseHandler}, +}; +use acpi::{Handle, Handler}; + +/// Simplifies the construction of a standard test [`Handler`]. +/// +/// By combining the commands and responses into a single tuple, it hopefully makes test files +/// easier to read and write. +pub type Command = (Check, Response); + +/// Construct a "standard handler". +/// +/// This is the handler that I expect to be used most often in tests. (A [`CheckCommandHandler`] +/// wrapping a [`ListedResponseHandler`]). +pub fn construct_std_handler(commands: Vec) -> impl Handler { + let (c, r): (Vec, Vec) = commands.into_iter().unzip(); + + CheckCommandHandler::new(c, ListedResponseHandler::new(r)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::map_physical_region`]. +pub const fn map_physical_region(physical_address: usize, size: usize) -> Command { + (Check::MapPhysicalRegion(physical_address, size), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::unmap_physical_region`]. +pub const fn unmap_physical_region(region: usize) -> Command { + (Check::UnmapPhysicalRegion(region), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_u8`]. +pub const fn read_u8(address: usize, response: u8) -> Command { + (Check::ReadU8(address), Response::ReadU8(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_u16`]. +pub const fn read_u16(address: usize, response: u16) -> Command { + (Check::ReadU16(address), Response::ReadU16(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_u32`]. +pub const fn read_u32(address: usize, response: u32) -> Command { + (Check::ReadU32(address), Response::ReadU32(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_u64`]. +pub const fn read_u64(address: usize, response: u64) -> Command { + (Check::ReadU64(address), Response::ReadU64(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_u8`]. +pub const fn write_u8(address: usize, value: u8) -> Command { + (Check::WriteU8(address, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_u16`]. +pub const fn write_u16(address: usize, value: u16) -> Command { + (Check::WriteU16(address, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_u32`]. +pub const fn write_u32(address: usize, value: u32) -> Command { + (Check::WriteU32(address, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_u64`]. +pub const fn write_u64(address: usize, value: u64) -> Command { + (Check::WriteU64(address, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_io_u8`]. +pub const fn read_io_u8(port: u16, response: u8) -> Command { + (Check::ReadIoU8(port), Response::ReadIoU8(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_io_u16`]. +pub const fn read_io_u16(port: u16, response: u16) -> Command { + (Check::ReadIoU16(port), Response::ReadIoU16(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_io_u32`]. +pub const fn read_io_u32(port: u16, response: u32) -> Command { + (Check::ReadIoU32(port), Response::ReadIoU32(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_io_u8`]. +pub const fn write_io_u8(port: u16, value: u8) -> Command { + (Check::WriteIoU8(port, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_io_u16`]. +pub const fn write_io_u16(port: u16, value: u16) -> Command { + (Check::WriteIoU16(port, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_io_u32`]. +pub const fn write_io_u32(port: u16, value: u32) -> Command { + (Check::WriteIoU32(port, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_pci_u8`]. +pub const fn read_pci_u8(address: PciAddress, offset: u16, response: u8) -> Command { + (Check::ReadPciU8(address, offset), Response::ReadPciU8(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_pci_u16`]. +pub const fn read_pci_u16(address: PciAddress, offset: u16, response: u16) -> Command { + (Check::ReadPciU16(address, offset), Response::ReadPciU16(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::read_pci_u32`]. +pub const fn read_pci_u32(address: PciAddress, offset: u16, response: u32) -> Command { + (Check::ReadPciU32(address, offset), Response::ReadPciU32(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_pci_u8`]. +pub const fn write_pci_u8(address: PciAddress, offset: u16, value: u8) -> Command { + (Check::WritePciU8(address, offset, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_pci_u16`]. +pub const fn write_pci_u16(address: PciAddress, offset: u16, value: u16) -> Command { + (Check::WritePciU16(address, offset, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::write_pci_u32`]. +pub const fn write_pci_u32(address: PciAddress, offset: u16, value: u32) -> Command { + (Check::WritePciU32(address, offset, value), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::nanos_since_boot`]. +pub const fn nanos_since_boot(response: u64) -> Command { + (Check::NanosSinceBoot, Response::NanosSinceBoot(response)) +} + +/// A simple helper to generate a [`Command`] for [`Handler::stall`]. +pub const fn stall(microseconds: u64) -> Command { + (Check::Stall(microseconds), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::sleep`]. +pub const fn sleep(milliseconds: u64) -> Command { + (Check::Sleep(milliseconds), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::create_mutex`]. +pub const fn create_mutex() -> Command { + (Check::CreateMutex, Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::acquire`]. +pub const fn acquire(mutex: Handle, timeout: u16) -> Command { + (Check::Acquire(mutex, timeout), Response::Skip()) +} + +/// A simple helper to generate a [`Command`] for [`Handler::release`]. +pub const fn release(mutex: Handle) -> Command { + (Check::Release(mutex), Response::Skip()) +} \ No newline at end of file diff --git a/tools/aml_test_tools/src/lib.rs b/tools/aml_test_tools/src/lib.rs new file mode 100644 index 00000000..33d2b0a9 --- /dev/null +++ b/tools/aml_test_tools/src/lib.rs @@ -0,0 +1,272 @@ +//! A collection of helper utilities for testing AML using the [`acpi`] crate. +//! +//! These utilities are very heavily based on the way the [`acpi`] crate has used them historically. +//! As always, feel free to offer PRs for improvements. + +pub mod handlers; + +use acpi::{ + Handler, + PhysicalMapping, + address::MappedGas, + aml::{AmlError, Interpreter, namespace::AmlName, object::Object}, +}; +use log::{error, trace}; +use std::{ + ffi::OsStr, + fs::File, + io::{Read, Write}, + path::PathBuf, + process::Command, + ptr::NonNull, + str::FromStr, + sync::Arc, +}; +use tempfile::{NamedTempFile, TempDir, tempdir}; + +/// Possible results of [`resolve_and_compile`]. +pub enum CompilationOutcome { + /// Not a valid ASL or AML file. + Ignored, + /// This file is already an AML file, does not need recompiling. + IsAml(PathBuf), + /// Both .asl and .aml files exist, and the .aml file is newer - it does not need recompiling. + Newer(PathBuf), + /// There's a .asl file only, but [`resolve_and_compile`] was called with `can_compile=false`. + NotCompiled(PathBuf), + /// Compilation was attempted but failed. The compilation output has been printed to stdout. + Failed(PathBuf), + /// An ASL file was found and compiled successfully. This variant contains the path to the + /// compiled AML file. + Succeeded(PathBuf), +} + +/// Possible results of [`run_test_for_file`]. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum TestResult { + /// The test passed. + Pass, + /// The test ASL failed compilation by `iasl`. + CompileFail, + /// Our interpreter failed to parse the resulting AML. + ParseFail, + // TODO: should we do this?? + NotCompiled, +} + +/// An internal-only struct that helps keep track of temporary files generated by +/// [`run_test_for_string`] +struct TempScriptFile { + // This appears to be unused, but since it deletes the temporary directory when it's dropped, we + // need to keep it. + #[allow(unused)] + dir: TempDir, + asl_file: NamedTempFile, + aml_file: PathBuf, +} + +/// Determine what to do with this file - ignore, compile and parse, or just parse. +/// If ".aml" does not exist, or if ".asl" is newer, compiles the file. +/// If the ".aml" file is newer, indicate it is ready to parse. +/// +/// Arguments: +/// - `path`: The filename of an ASL file to (possibly) compile. +/// - `can_compile`: Whether compilation is allowed for this file. If not, any existing AML file +/// with the same name (but different extension) will be used. +pub fn resolve_and_compile(path: &PathBuf, can_compile: bool) -> std::io::Result { + // If this file is aml and it exists, it's ready for parsing + // metadata() will error if the file does not exist + if path.extension() == Some(OsStr::new("aml")) && path.metadata()?.is_file() { + return Ok(CompilationOutcome::IsAml(path.clone())); + } + + // If this file is not asl, it's not interesting. Error if the file does not exist. + if path.extension() != Some(OsStr::new("asl")) || !path.metadata()?.is_file() { + return Ok(CompilationOutcome::Ignored); + } + + let aml_path = path.with_extension("aml"); + + if aml_path.is_file() { + let asl_last_modified = path.metadata()?.modified()?; + let aml_last_modified = aml_path.metadata()?.modified()?; + // If the aml is more recent than the asl, use the existing aml + // Otherwise continue to compilation + if asl_last_modified <= aml_last_modified { + return Ok(CompilationOutcome::Newer(aml_path)); + } + } + + if !can_compile { + return Ok(CompilationOutcome::NotCompiled(path.clone())); + } + + // Compile the ASL file using `iasl` + println!("Compiling file: {}", path.display()); + let output = Command::new("iasl").arg(path).output()?; + + if !output.status.success() { + println!( + "Failed to compile ASL file: {}. Output from iasl:\n {}", + path.display(), + String::from_utf8_lossy(&output.stderr) + ); + Ok(CompilationOutcome::Failed(path.clone())) + } else { + Ok(CompilationOutcome::Succeeded(aml_path)) + } +} + +/// Construct a new [interpreter](Interpreter) to use for testing. +/// +/// This interpreter uses some simple fake registers and FACS to facilitate testing. It is not +/// mandatory to use this interpreter for testing when using other functions in this crate. +/// +/// Arguments: +/// +/// * `handler`: The Handler to be called by the interpreter when needed. This crate includes some +/// example [handlers]. +pub fn new_interpreter(handler: T) -> Interpreter +where + T: Handler + Clone, +{ + let fake_registers = Arc::new(acpi::registers::FixedRegisters { + pm1_event_registers: acpi::registers::Pm1EventRegisterBlock { + pm1_event_length: 8, + pm1a: unsafe { + MappedGas::map_gas( + acpi::address::GenericAddress { + address_space: acpi::address::AddressSpace::SystemIo, + bit_width: 32, + bit_offset: 0, + access_size: 1, + address: 0x400, + }, + &handler, + ) + .unwrap() + }, + pm1b: None, + }, + pm1_control_registers: acpi::registers::Pm1ControlRegisterBlock { + pm1a: unsafe { + MappedGas::map_gas( + acpi::address::GenericAddress { + address_space: acpi::address::AddressSpace::SystemIo, + bit_width: 32, + bit_offset: 0, + access_size: 1, + address: 0x600, + }, + &handler, + ) + .unwrap() + }, + pm1b: None, + }, + }); + + // This PhysicalMapping is dropped when the interpreter is dropped, and if you use logging in + // the handler object you'll see a call to Handler::unmap_physical_region without any + // corresponding call to Interpreter::map_physical_region. + let fake_facs = PhysicalMapping { + physical_start: 0x0, + virtual_start: NonNull::new(0x8000_0000_0000_0000 as *mut acpi::sdt::facs::Facs).unwrap(), + region_length: 32, + mapped_length: 32, + handler: handler.clone(), + }; + Interpreter::new(handler, 2, fake_registers, Some(fake_facs)) +} + +/// Test an ASL script given as a string, using [`run_test`]. +/// +/// Arguments: +/// * `asl`: A string slice containing an ASL script. This will be compiled to AML using `iasl` and +/// then tested using [`run_test`] +/// * `interpreter`: The interpreter to use for testing. +pub fn run_test_for_string(asl: &'static str, interpreter: &mut Interpreter) -> TestResult { + let script = create_script_file(asl); + resolve_and_compile(&script.asl_file.path().to_path_buf(), true).unwrap(); + + run_test_for_file(&script.aml_file, interpreter) +} + +/// Test an AML file using [`run_test`] +/// +/// Arguments: +/// +/// * `file`: The path to the AML file to test. This must be an AML file otherwise the test will +/// fail very quickly. +/// * `interpreter`: The interpreter to use for testing. +pub fn run_test_for_file(file: &PathBuf, interpreter: &mut Interpreter) -> TestResult { + let Ok(mut file) = File::open(&file) else { + return TestResult::CompileFail; + }; + let mut contents = Vec::new(); + file.read_to_end(&mut contents).unwrap(); + + const AML_TABLE_HEADER_LENGTH: usize = 36; + let stream = &contents[AML_TABLE_HEADER_LENGTH..]; + + match run_test(stream, interpreter) { + Ok(()) => TestResult::Pass, + Err(e) => { + error!("Error running test: {:?}", e); + TestResult::ParseFail + } + } +} + +/// Internal function to create a temporary script file from an ASL string, plus to calculate the +/// name of the post-compilation AML file. +fn create_script_file(asl: &'static str) -> TempScriptFile { + let dir = tempdir().unwrap(); + trace!("Created temp dir: {:?}", dir.path()); + let mut script_file = NamedTempFile::with_suffix_in(".asl", &dir).unwrap(); + trace!("Created temp file: {:?}", script_file.path()); + + let output_stem = script_file.path().file_stem().unwrap().to_str().unwrap(); + let output_name = format!("{}.aml", output_stem); + let output_full_name = dir.path().join(output_name.clone()); + + let new_asl = asl.replace("%FN%", output_name.as_str()); + + script_file.write_all(new_asl.as_bytes()).unwrap(); + script_file.flush().unwrap(); + + TempScriptFile { dir, asl_file: script_file, aml_file: output_full_name } +} + +/// Run a test on an AML stream. +/// +/// In this context, "a test" means: +/// +/// * Attempting to parse the AML +/// * If the AML contains a "MAIN" method, attempting to execute it. +/// +/// Arguments: +/// +/// * `stream`: A slice containing the AML bytecode to test. +/// * `interpreter`: The interpreter to test with. +pub fn run_test(stream: &[u8], interpreter: &mut Interpreter) -> Result<(), AmlError> { + interpreter.load_table(stream)?; + + if let Some(result) = interpreter.evaluate_if_present(AmlName::from_str("\\MAIN").unwrap(), vec![])? { + match *result { + Object::Integer(0) => Ok(()), + Object::Integer(other) => { + error!("Test _MAIN returned non-zero exit code: {}", other); + // TODO: wrong error - this should probs return a more complex err type + Err(AmlError::NoCurrentOp) + } + _ => { + error!("Test _MAIN returned unexpected object type: {}", *result); + // TODO: wrong error + Err(AmlError::NoCurrentOp) + } + } + } else { + Ok(()) + } +} diff --git a/tools/aml_tester/Cargo.toml b/tools/aml_tester/Cargo.toml index 71c7f1c7..c38f20ec 100644 --- a/tools/aml_tester/Cargo.toml +++ b/tools/aml_tester/Cargo.toml @@ -6,7 +6,8 @@ edition = "2018" [dependencies] acpi = { path = "../.." } +aml_test_tools = { path = "../aml_test_tools" } clap = "4" colored = "3.1.1" log = "0.4" -pci_types = "0.10" +pretty_env_logger = "0.5.0" diff --git a/tools/aml_tester/src/main.rs b/tools/aml_tester/src/main.rs index 0ed19b1b..82e6e3d2 100644 --- a/tools/aml_tester/src/main.rs +++ b/tools/aml_tester/src/main.rs @@ -9,42 +9,34 @@ * - For failing tests, print out a nice summary of the errors for each file */ -use acpi::{ - address::MappedGas, - aml::{namespace::AmlName, object::Object, AmlError, Interpreter}, - Handle, - PhysicalMapping, +use acpi::Handler; +use aml_test_tools::{ + handlers::{logging_handler::LoggingHandler, null_handler::NullHandler}, + new_interpreter, + resolve_and_compile, + CompilationOutcome, + TestResult, }; use clap::{Arg, ArgAction, ArgGroup}; use colored::Colorize; -use log::{error, info}; -use pci_types::PciAddress; use std::{ collections::HashSet, - ffi::OsStr, - fs::{self, File}, - io::{Read, Write}, + fs::{self}, + io::Write, path::{Path, PathBuf}, process::Command, - ptr::NonNull, - str::FromStr, - sync::Arc, }; -enum CompilationOutcome { - Ignored, - IsAml(PathBuf), - Newer(PathBuf), - NotCompiled(PathBuf), - Failed(PathBuf), - Succeeded(PathBuf), -} - fn main() -> std::io::Result<()> { + pretty_env_logger::init(); + let mut cmd = clap::Command::new("aml_tester") .version("v0.1.0") .author("Isaac Woods") - .about("Compiles and tests ASL files") + .about( + "Compiles ASL files and checks that they can be parsed by the ACPI crate. +If the ASL contains a MAIN method, it will be executed.", + ) .arg(Arg::new("no_compile").long("no-compile").action(ArgAction::SetTrue).help("Don't compile ASL to AML")) .arg( Arg::new("combined") @@ -59,8 +51,7 @@ fn main() -> std::io::Result<()> { cmd.print_help()?; return Ok(()); } - log::set_logger(&Logger).unwrap(); - log::set_max_level(log::LevelFilter::Trace); + log::set_max_level(log::LevelFilter::Info); let matches = cmd.get_matches(); @@ -106,18 +97,6 @@ fn main() -> std::io::Result<()> { } } - #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] - enum TestResult { - /// The test passed. - Pass, - /// The test ASL failed compilation by `iasl`. - CompileFail, - /// Our interpreter failed to parse the resulting AML. - ParseFail, - // TODO: should we do this?? - NotCompiled, - } - // Make a list of the files we have processed, and skip them if we see them again let mut dedup_list: HashSet = HashSet::new(); let mut summaries: HashSet<(PathBuf, TestResult)> = HashSet::new(); @@ -149,41 +128,32 @@ fn main() -> std::io::Result<()> { .collect::>(); let combined_test = matches.get_flag("combined"); - let mut interpreter = new_interpreter(); + let mut interpreter = new_interpreter(new_handler()); let (passed, failed) = aml_files.into_iter().fold((0, 0), |(passed, failed), file_entry| { print!("Testing AML file: {:?}... ", file_entry); std::io::stdout().flush().unwrap(); - let Ok(mut file) = File::open(&file_entry) else { - summaries.insert((file_entry, TestResult::CompileFail)); - return (passed, failed + 1); - }; - let mut contents = Vec::new(); - file.read_to_end(&mut contents).unwrap(); - if !combined_test { - interpreter = new_interpreter(); + interpreter = new_interpreter(new_handler()); } - const AML_TABLE_HEADER_LENGTH: usize = 36; - let stream = &contents[AML_TABLE_HEADER_LENGTH..]; - - match run_test(stream, &mut interpreter) { - Ok(()) => { + let result = aml_test_tools::run_test_for_file(&file_entry, &mut interpreter); + let updates = match result { + TestResult::Pass => { println!("{}", "OK".green()); - println!("Namespace: {}", interpreter.namespace.lock()); - summaries.insert((file_entry, TestResult::Pass)); (passed + 1, failed) } - - Err(err) => { - println!("{}", format!("Failed ({:?})", err).red()); - println!("Namespace: {}", interpreter.namespace.lock()); - summaries.insert((file_entry, TestResult::ParseFail)); + TestResult::CompileFail | TestResult::ParseFail | TestResult::NotCompiled => { + println!("{}", format!("Failed ({:?})", result).red()); (passed, failed + 1) } - } + }; + + println!("Namespace: {}", interpreter.namespace.lock()); + summaries.insert((file_entry, result)); + + updates }); // Print summaries @@ -209,74 +179,6 @@ fn main() -> std::io::Result<()> { Ok(()) } -fn new_interpreter() -> Interpreter { - let fake_registers = Arc::new(acpi::registers::FixedRegisters { - pm1_event_registers: acpi::registers::Pm1EventRegisterBlock { - pm1_event_length: 8, - pm1a: unsafe { - MappedGas::map_gas( - acpi::address::GenericAddress { - address_space: acpi::address::AddressSpace::SystemIo, - bit_width: 32, - bit_offset: 0, - access_size: 1, - address: 0x400, - }, - &Handler, - ) - .unwrap() - }, - pm1b: None, - }, - pm1_control_registers: acpi::registers::Pm1ControlRegisterBlock { - pm1a: unsafe { - MappedGas::map_gas( - acpi::address::GenericAddress { - address_space: acpi::address::AddressSpace::SystemIo, - bit_width: 32, - bit_offset: 0, - access_size: 1, - address: 0x600, - }, - &Handler, - ) - .unwrap() - }, - pm1b: None, - }, - }); - let fake_facs = PhysicalMapping { - physical_start: 0x0, - virtual_start: NonNull::new(0x8000_0000_0000_0000 as *mut acpi::sdt::facs::Facs).unwrap(), - region_length: 32, - mapped_length: 32, - handler: Handler, - }; - Interpreter::new(Handler, 2, fake_registers, Some(fake_facs)) -} - -fn run_test(stream: &[u8], interpreter: &mut Interpreter) -> Result<(), AmlError> { - interpreter.load_table(stream)?; - - if let Some(result) = interpreter.evaluate_if_present(AmlName::from_str("\\MAIN").unwrap(), vec![])? { - match *result { - Object::Integer(0) => Ok(()), - Object::Integer(other) => { - error!("Test _MAIN returned non-zero exit code: {}", other); - // TODO: wrong error - this should probs return a more complex err type - Err(AmlError::NoCurrentOp) - } - _ => { - error!("Test _MAIN returned unexpected object type: {}", *result); - // TODO: wrong error - Err(AmlError::NoCurrentOp) - } - } - } else { - Ok(()) - } -} - fn find_tests(matches: &clap::ArgMatches) -> std::io::Result> { // Get an initial list of files - may not work correctly on non-UTF8 OsString let files: Vec = if matches.contains_id("path") { @@ -307,175 +209,6 @@ fn find_tests(matches: &clap::ArgMatches) -> std::io::Result> { Ok(files) } -/// Determine what to do with this file - ignore, compile and parse, or just parse. -/// If ".aml" does not exist, or if ".asl" is newer, compiles the file. -/// If the ".aml" file is newer, indicate it is ready to parse. -fn resolve_and_compile(path: &PathBuf, can_compile: bool) -> std::io::Result { - // If this file is aml and it exists, it's ready for parsing - // metadata() will error if the file does not exist - if path.extension() == Some(OsStr::new("aml")) && path.metadata()?.is_file() { - return Ok(CompilationOutcome::IsAml(path.clone())); - } - - // If this file is not asl, it's not interesting. Error if the file does not exist. - if path.extension() != Some(OsStr::new("asl")) || !path.metadata()?.is_file() { - return Ok(CompilationOutcome::Ignored); - } - - let aml_path = path.with_extension("aml"); - - if aml_path.is_file() { - let asl_last_modified = path.metadata()?.modified()?; - let aml_last_modified = aml_path.metadata()?.modified()?; - // If the aml is more recent than the asl, use the existing aml - // Otherwise continue to compilation - if asl_last_modified <= aml_last_modified { - return Ok(CompilationOutcome::Newer(aml_path)); - } - } - - if !can_compile { - return Ok(CompilationOutcome::NotCompiled(path.clone())); - } - - // Compile the ASL file using `iasl` - println!("Compiling file: {}", path.display()); - let output = Command::new("iasl").arg(path).output()?; - - if !output.status.success() { - println!( - "Failed to compile ASL file: {}. Output from iasl:\n {}", - path.display(), - String::from_utf8_lossy(&output.stderr) - ); - Ok(CompilationOutcome::Failed(path.clone())) - } else { - Ok(CompilationOutcome::Succeeded(aml_path)) - } -} - -struct Logger; - -impl log::Log for Logger { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - println!("[{}] {}", record.level(), record.args()); - } - - fn flush(&self) { - std::io::stdout().flush().unwrap(); - } -} - -#[derive(Clone)] -struct Handler; - -impl acpi::Handler for Handler { - unsafe fn map_physical_region(&self, _physical_address: usize, _size: usize) -> PhysicalMapping { - todo!() - } - - fn unmap_physical_region(_region: &PhysicalMapping) {} - - fn read_u8(&self, address: usize) -> u8 { - println!("read_u8 {address:#x}"); - 0 - } - fn read_u16(&self, address: usize) -> u16 { - println!("read_u16 {address:#x}"); - 0 - } - fn read_u32(&self, address: usize) -> u32 { - println!("read_u32 {address:#x}"); - 0 - } - fn read_u64(&self, address: usize) -> u64 { - println!("read_u64 {address:#x}"); - 0 - } - - fn write_u8(&self, address: usize, value: u8) { - println!("write_u8 {address:#x}<-{value:#x}"); - } - fn write_u16(&self, address: usize, value: u16) { - println!("write_u16 {address:#x}<-{value:#x}"); - } - fn write_u32(&self, address: usize, value: u32) { - println!("write_u32 {address:#x}<-{value:#x}"); - } - fn write_u64(&self, address: usize, value: u64) { - println!("write_u64 {address:#x}<-{value:#x}"); - } - - fn read_io_u8(&self, port: u16) -> u8 { - println!("read_io_u8 {port:#x}"); - 0 - } - fn read_io_u16(&self, port: u16) -> u16 { - println!("read_io_u16 {port:#x}"); - 0 - } - fn read_io_u32(&self, port: u16) -> u32 { - println!("read_io_u32 {port:#x}"); - 0 - } - - fn write_io_u8(&self, port: u16, value: u8) { - println!("write_io_u8 {port:#x}<-{value:#x}"); - } - fn write_io_u16(&self, port: u16, value: u16) { - println!("write_io_u16 {port:#x}<-{value:#x}"); - } - fn write_io_u32(&self, port: u16, value: u32) { - println!("write_io_u32 {port:#x}<-{value:#x}"); - } - - fn read_pci_u8(&self, address: PciAddress, _offset: u16) -> u8 { - println!("read_pci_u8 ({address})"); - 0 - } - fn read_pci_u16(&self, address: PciAddress, _offset: u16) -> u16 { - println!("read_pci_u16 ({address})"); - 0 - } - fn read_pci_u32(&self, address: PciAddress, _offset: u16) -> u32 { - println!("read_pci_u32 ({address})"); - 0 - } - - fn write_pci_u8(&self, address: PciAddress, _offset: u16, value: u8) { - println!("write_pci_u8 ({address})<-{value}"); - } - fn write_pci_u16(&self, address: PciAddress, _offset: u16, value: u16) { - println!("write_pci_u16 ({address})<-{value}"); - } - fn write_pci_u32(&self, address: PciAddress, _offset: u16, value: u32) { - println!("write_pci_u32 ({address})<-{value}"); - } - - fn handle_debug(&self, object: &Object) { - info!("Debug store: {}", object); - } - - fn nanos_since_boot(&self) -> u64 { - 0 - } - - fn stall(&self, microseconds: u64) { - println!("Stalling for {}us", microseconds); - } - fn sleep(&self, milliseconds: u64) { - println!("Sleeping for {}ms", milliseconds); - } - - fn create_mutex(&self) -> Handle { - Handle(0) - } - fn acquire(&self, _mutex: Handle, _timeout: u16) -> Result<(), AmlError> { - Ok(()) - } - fn release(&self, _mutex: Handle) {} +fn new_handler() -> impl Handler { + LoggingHandler::new(NullHandler {}) }