diff --git a/examples/tests/Cargo.toml b/examples/tests/Cargo.toml index 3735e63c..0b6a6623 100644 --- a/examples/tests/Cargo.toml +++ b/examples/tests/Cargo.toml @@ -15,22 +15,22 @@ tokio = { version = "1", default-features = false, features = [ "time", "rt-multi-thread", "macros", + "process" ] } +pretty_env_logger = "0.5.0" reqwest = "0.12" +hilbench-agent = "0.1.0" embedded-io-adapters = { version = "0.6.1", features = ["tokio-1"] } embedded-io-async = { version = "0.6.1" } embedded-io = { version = "0.6.1" } -embassy-sync = { version = "0.6" } -tokio-serial = "5.4" critical-section = { version = "1", features = ["std"] } -probe-rs = "0.25.0" +embassy-sync = "0.6" +tokio-serial = "5.4" +tokio-util = "0.7" rand = "0.8.5" heapless = "0.8.0" anyhow = "1" -object = "0.32.2" -defmt-decoder = { version = "0.3.9", features = ["unstable"] } -bytes = "1.5.0" -backtrace = "0.3.69" -pretty_env_logger = "0.5.0" -pin-project-lite = "0.2.13" -chrono = { version = "0.4.31", features = ["serde"] } +tempfile = "3.15" + +[patch.crates-io] +hilbench-agent = { git = "https://github.com/lulf/hilbench.git", rev = "700693cec2f813967f6717341296828d4c2971ae" } diff --git a/examples/tests/src/lib.rs b/examples/tests/src/lib.rs index 6b814ded..dbccd3ee 100644 --- a/examples/tests/src/lib.rs +++ b/examples/tests/src/lib.rs @@ -1,2 +1,33 @@ +use anyhow::anyhow; +use hilbench_agent::ProbeConfig; +use probe::DeviceUnderTest; + pub mod probe; pub mod serial; + +pub struct TestContext { + pub serial_adapters: Vec, + pub probe_config: ProbeConfig, +} + +impl TestContext { + pub fn new() -> Self { + let serial_adapters = serial::find_controllers(); + let config = std::env::var("PROBE_CONFIG").unwrap(); + log::info!("Using probe config {}", config); + let probe_config = serde_json::from_str(&config).unwrap(); + + Self { + serial_adapters, + probe_config, + } + } + + pub fn find_dut(&self, labels: &[(&str, &str)]) -> Result, anyhow::Error> { + let selector = hilbench_agent::init(self.probe_config.clone()); + let target = selector + .select(labels) + .ok_or(anyhow!("Unable to find DUT for {:?}", labels))?; + Ok(DeviceUnderTest::new(target)) + } +} diff --git a/examples/tests/src/probe/mod.rs b/examples/tests/src/probe/mod.rs index e5e94bf4..d147fd3b 100644 --- a/examples/tests/src/probe/mod.rs +++ b/examples/tests/src/probe/mod.rs @@ -1,136 +1,80 @@ -use probe_rs::flashing::Format; -use probe_rs::probe::list::Lister; -use probe_rs::probe::DebugProbeSelector; -use probe_rs::{Permissions, Session}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::OnceLock; -use tokio::sync::oneshot; -use tokio::task::spawn_blocking; - -mod run; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ProbeConfig { - pub targets: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct TargetConfig { - pub chip: String, - pub probe: String, - pub labels: HashMap, -} - -static SELECTOR: OnceLock = OnceLock::new(); - -pub fn init(config: ProbeConfig) -> &'static ProbeSelector { - SELECTOR.get_or_init(|| ProbeSelector::new(config)) -} - -pub struct ProbeSelector { - targets: Vec<(AtomicBool, TargetConfig)>, -} - -#[derive(Debug)] -pub struct Target<'d> { - config: TargetConfig, - taken: &'d AtomicBool, +use std::io::Write; +use std::process::Stdio; +use tokio::io::AsyncBufReadExt; +use tokio::io::BufReader; +use tokio::process::Command; +use tokio::select; +use tokio_util::sync::CancellationToken; + +use hilbench_agent::ProbeConfig; +use hilbench_agent::Target; + +pub fn init(config: ProbeConfig) { + hilbench_agent::init(config); } pub struct Firmware { pub data: Vec, - pub format: Format, } -impl ProbeSelector { - fn new(config: ProbeConfig) -> Self { - let mut targets = Vec::new(); - for t in config.targets { - targets.push((AtomicBool::new(false), t)); - } - Self { targets } - } - - /// Select a target with the provided labels - pub fn select<'m>(&'m self, labels: &[(&str, &str)]) -> Option> { - for (taken, config) in &self.targets { - let mut matched = true; - for (key, value) in labels { - let v = config.labels.get(*key); - if let Some(v) = v { - if v != value { - matched = false; - break; - } - } - } - if matched && taken.swap(true, Ordering::Acquire) == false { - return Some(Target { - config: config.clone(), - taken, - }); - } - } - None - } +pub struct DeviceUnderTest<'d> { + target: Target<'d>, + token: CancellationToken, } -impl<'d> Target<'d> { - pub fn flash(self, fw: Firmware) -> Result, anyhow::Error> { - let probe = self.config.probe.clone(); - let p: DebugProbeSelector = probe.try_into()?; - log::info!("Debug probe selector created"); - let t = probe_rs::config::get_target_by_name(&self.config.chip)?; - log::info!("Target created"); - - let lister = Lister::new(); - log::info!("Opening probe"); - let probe = lister.open(p)?; - - let perms = Permissions::new().allow_erase_all(); - log::info!("Attaching probe"); - let mut session = probe.attach(t, perms)?; - let mut flasher = run::Flasher::new(fw); - flasher.flash(&mut session)?; - Ok(TargetRunner { - _target: self, - flasher, - session, - }) +impl<'d> DeviceUnderTest<'d> { + pub(crate) fn new(target: Target<'d>) -> Self { + Self { + target, + token: CancellationToken::new(), + } } -} - -impl<'d> Drop for Target<'d> { - fn drop(&mut self) { - self.taken.store(false, Ordering::Release); + pub fn token(&self) -> CancellationToken { + self.token.clone() } -} - -pub struct TargetRunner<'d> { - _target: Target<'d>, - flasher: run::Flasher, - session: Session, -} -impl<'d> TargetRunner<'d> { - pub async fn run(mut self, cancel: oneshot::Receiver<()>) -> Result<(), anyhow::Error> { - let result = spawn_blocking(move || { - let mut runner = self.flasher.start(&mut self.session).unwrap(); - runner.run(&mut self.session, cancel) - }) - .await - .unwrap(); - match result { - Ok(halted) => { - if halted { - Err(anyhow::anyhow!("Firmware stopped")) - } else { - Ok(()) + pub async fn run(self, fw: Firmware) -> Result { + let mut temp = tempfile::NamedTempFile::new()?; + temp.write_all(&fw.data)?; + let path = temp.path().to_str().unwrap().to_string(); + drop(temp); + let mut flasher = Command::new("probe-rs") + .env("RUST_LOG", "info") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("run") + .arg("--elf") + .arg(&path) + .arg("--chip") + .arg(&self.target.config().chip) + .arg("--probe") + .arg(&self.target.config().probe) + .spawn() + .unwrap(); + + let stderr = flasher.stderr.as_mut().unwrap(); + let mut stderr_reader = BufReader::new(stderr); + + let mut lines: Vec = Vec::new(); + select! { + _ = self.token.cancelled() => { + flasher.kill().await.unwrap(); + } + _ = async { + loop { + let mut line = String::new(); + stderr_reader.read_line(&mut line).await.unwrap(); + lines.push(line); } + } => { + } - Err(e) => Err(e.into()), } + flasher.wait().await.unwrap(); + Ok(FirmwareLogs { lines }) } } + +pub struct FirmwareLogs { + pub lines: Vec, +} diff --git a/examples/tests/src/probe/run.rs b/examples/tests/src/probe/run.rs deleted file mode 100644 index 54dcd080..00000000 --- a/examples/tests/src/probe/run.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::collections::BTreeMap; -use std::fmt::Write; -use std::io::Cursor; -use std::time::Duration; - -use super::Firmware; -use anyhow::{anyhow, bail}; -use defmt_decoder::{DecodeError, Location, StreamDecoder, Table}; -use log::info; -use object::read::{File as ElfFile, Object as _}; -use object::ObjectSymbol; -use probe_rs::debug::{DebugInfo, DebugRegisters}; -use probe_rs::flashing::{DownloadOptions, Format}; -use probe_rs::rtt::{Rtt, ScanRegion}; -use probe_rs::{Core, MemoryInterface, Session}; -use tokio::sync::oneshot; - -const THUMB_BIT: u32 = 1; -const TIMEOUT: Duration = Duration::from_secs(1); - -const POLL_SLEEP_MILLIS: u64 = 100; - -pub(crate) struct Flasher { - firmware: Firmware, -} - -pub(crate) struct Runner { - elf: Option, -} - -struct ElfRunner { - rtt: Rtt, - di: DebugInfo, - defmt_table: Box, - defmt_locs: BTreeMap, - defmt_stream: Box, -} - -unsafe fn fuck_it<'a, 'b, T>(wtf: &'a T) -> &'b T { - std::mem::transmute(wtf) -} - -impl Flasher { - pub fn new(firmware: Firmware) -> Self { - Self { firmware } - } - - pub fn flash(&mut self, sess: &mut Session) -> anyhow::Result<()> { - // reset ALL cores other than the main one. - // This is needed for rp2040 core1. - for (i, _) in sess.list_cores() { - if i != 0 { - sess.core(i)?.reset()?; - } - } - - sess.core(0)?.reset_and_halt(TIMEOUT)?; - - log::info!("flashing program..."); - let mut dopts = DownloadOptions::new(); - dopts.keep_unwritten_bytes = true; - dopts.verify = true; - - let mut loader = sess.target().flash_loader(); - let instruction_set = sess.core(0)?.instruction_set().ok(); - loader.load_image( - sess, - &mut Cursor::new(&self.firmware.data), - self.firmware.format.clone(), - instruction_set, - )?; - loader.commit(sess, dopts)?; - - //flashing::download_file_with_options(sess, &opts.elf, Format::Elf, dopts)?; - log::info!("flashing done!"); - - Ok(()) - } - - pub(crate) fn start(&mut self, sess: &mut Session) -> anyhow::Result { - if self.firmware.format == Format::Elf { - let elf_bytes = &self.firmware.data[..]; - let elf = ElfFile::parse(elf_bytes)?; - let di = DebugInfo::from_raw(elf_bytes)?; - - let table = Box::new(defmt_decoder::Table::parse(elf_bytes)?.unwrap()); - let locs = table.get_locations(elf_bytes)?; - if !table.is_empty() && locs.is_empty() { - log::warn!("insufficient DWARF info; compile your program with `debug = 2` to enable location info"); - } - - let (rtt_addr, main_addr) = get_rtt_main_from(&elf)?; - let rtt_addr = rtt_addr.ok_or_else(|| anyhow!("RTT is missing"))?; - - { - let mut core = sess.core(0)?; - - core.reset_and_halt(TIMEOUT)?; - - log::debug!("starting device"); - if core.available_breakpoint_units()? == 0 { - bail!("RTT not supported on device without HW breakpoints"); - } - - // Corrupt the rtt control block so that it's setup fresh again - // Only do this when running from flash, because when running from RAM the - // "fake-flashing to RAM" is what initializes it. - core.write_word_32(rtt_addr as _, 0xdeadc0de)?; - - // RTT control block is initialized pre-main. Run until main before - // changing to BlockIfFull. - core.set_hw_breakpoint(main_addr as _)?; - core.run()?; - core.wait_for_core_halted(Duration::from_secs(5))?; - core.clear_hw_breakpoint(main_addr as _)?; - - const OFFSET: u32 = 44; - const FLAG: u32 = 2; // BLOCK_IF_FULL - core.write_word_32((rtt_addr + OFFSET) as _, FLAG)?; - - core.run()?; - } - - let rtt = setup_logging_channel(rtt_addr as u64, sess)?; - let defmt_stream = unsafe { fuck_it(&table) }.new_stream_decoder(); - - Ok(Runner { - elf: Some(ElfRunner { - defmt_table: table, - defmt_locs: locs, - rtt, - defmt_stream, - di, - }), - }) - } else { - let mut core = sess.core(0)?; - core.reset_and_halt(TIMEOUT)?; - core.run()?; - Ok(Runner { elf: None }) - } - } -} - -impl ElfRunner { - fn poll(&mut self, sess: &mut Session) -> anyhow::Result<()> { - let current_dir = std::env::current_dir()?; - - let mut read_buf = [0; 1024]; - let defmt = self - .rtt - .up_channel(0) - .ok_or_else(|| anyhow!("RTT up channel 0 not found"))?; - match defmt.read(&mut sess.core(0).unwrap(), &mut read_buf)? { - 0 => { - // Sleep to reduce CPU usage when defmt didn't return any data. - std::thread::sleep(Duration::from_millis(POLL_SLEEP_MILLIS)); - return Ok(()); - } - n => self.defmt_stream.received(&read_buf[..n]), - } - - loop { - match self.defmt_stream.decode() { - Ok(frame) => { - let loc = self.defmt_locs.get(&frame.index()); - - let (mut file, mut line) = (None, None); - if let Some(loc) = loc { - let relpath = if let Ok(relpath) = loc.file.strip_prefix(¤t_dir) { - relpath - } else { - // not relative; use full path - &loc.file - }; - file = Some(relpath.display().to_string()); - line = Some(loc.line as u32); - }; - - let mut timestamp = String::new(); - if let Some(ts) = frame.display_timestamp() { - timestamp = format!("{} ", ts); - } - - log::logger().log( - &log::Record::builder() - .level(match frame.level() { - Some(level) => match level.as_str() { - "trace" => log::Level::Trace, - "debug" => log::Level::Debug, - "info" => log::Level::Info, - "warn" => log::Level::Warn, - "error" => log::Level::Error, - _ => log::Level::Error, - }, - None => log::Level::Info, - }) - .file(file.as_deref()) - .line(line) - .target("device") - //.args(format_args!("{} {:?} {:?}", frame.display_message(), file, line)) - .args(format_args!("{}{}", timestamp, frame.display_message())) - .build(), - ); - } - Err(DecodeError::UnexpectedEof) => break, - Err(DecodeError::Malformed) => match self.defmt_table.encoding().can_recover() { - // if recovery is impossible, abort - false => bail!("failed to decode defmt data"), - // if recovery is possible, skip the current frame and continue with new data - true => log::warn!("failed to decode defmt data"), - }, - } - } - - Ok(()) - } - - fn traceback(&mut self, core: &mut Core) -> anyhow::Result<()> { - let mut r = [0; 17]; - for (i, val) in r.iter_mut().enumerate() { - *val = core.read_core_reg::(i as u16)?; - } - info!( - " R0: {:08x} R1: {:08x} R2: {:08x} R3: {:08x}", - r[0], r[1], r[2], r[3], - ); - info!( - " R4: {:08x} R5: {:08x} R6: {:08x} R7: {:08x}", - r[4], r[5], r[6], r[7], - ); - info!( - " R8: {:08x} R9: {:08x} R10: {:08x} R11: {:08x}", - r[8], r[9], r[10], r[11], - ); - info!( - " R12: {:08x} SP: {:08x} LR: {:08x} PC: {:08x}", - r[12], r[13], r[14], r[15], - ); - info!("XPSR: {:08x}", r[16]); - - info!(""); - info!("Stack:"); - let mut stack = [0u32; 32]; - core.read_32(r[13] as _, &mut stack)?; - for i in 0..(stack.len() / 4) { - info!( - "{:08x}: {:08x} {:08x} {:08x} {:08x}", - r[13] + i as u32 * 16, - stack[i * 4 + 0], - stack[i * 4 + 1], - stack[i * 4 + 2], - stack[i * 4 + 3], - ); - } - - info!(""); - info!("Backtrace:"); - let di = &self.di; - let initial_registers = DebugRegisters::from_core(core); - let exception_handler = probe_rs::exception_handler_for_core(core.core_type()); - let instruction_set = core.instruction_set().ok(); - let stack_frames = di.unwind(core, initial_registers, exception_handler.as_ref(), instruction_set)?; - - for (i, frame) in stack_frames.iter().enumerate() { - let mut s = String::new(); - write!(&mut s, "Frame {}: {} @ {}", i, frame.function_name, frame.pc).unwrap(); - - if frame.is_inlined { - write!(&mut s, " inline").unwrap(); - } - info!("{}", s); - - if let Some(location) = &frame.source_location { - let mut s = String::new(); - let file = location.path.to_string_lossy(); - write!(&mut s, " {file}").unwrap(); - - if let Some(line) = location.line { - write!(&mut s, ":{line}").unwrap(); - if let Some(col) = location.column { - match col { - probe_rs::debug::ColumnType::LeftEdge => { - write!(&mut s, ":1").unwrap(); - } - probe_rs::debug::ColumnType::Column(c) => { - write!(&mut s, ":{c}").unwrap(); - } - } - } - } - info!("{}", s); - } - } - - Ok(()) - } -} - -impl Runner { - fn poll(&mut self, sess: &mut Session) -> anyhow::Result<()> { - if let Some(elf) = self.elf.as_mut() { - return elf.poll(sess); - } - Ok(()) - } - - pub(crate) fn run(&mut self, sess: &mut Session, mut cancel: oneshot::Receiver<()>) -> anyhow::Result { - let mut was_halted = false; - - loop { - match cancel.try_recv() { - Ok(_) | Err(oneshot::error::TryRecvError::Closed) => { - break; - } - _ => {} - } - - self.poll(sess)?; - - let mut core = sess.core(0)?; - let is_halted = core.core_halted()?; - - if is_halted && was_halted { - break; - } - was_halted = is_halted; - } - if was_halted { - let mut core = sess.core(0)?; - if let Some(elf) = self.elf.as_mut() { - elf.traceback(&mut core)?; - } - } - - Ok(was_halted) - } -} - -fn setup_logging_channel(rtt_addr: u64, sess: &mut Session) -> anyhow::Result { - const NUM_RETRIES: usize = 10; // picked at random, increase if necessary - let mut rtt_res: Result = Err(probe_rs::rtt::Error::ControlBlockNotFound); - - let mut core = sess.core(0).unwrap(); - - for try_index in 0..=NUM_RETRIES { - rtt_res = Rtt::attach_region(&mut core, &ScanRegion::Exact(rtt_addr)); - match rtt_res { - Ok(_) => { - log::debug!("Successfully attached RTT"); - break; - } - Err(probe_rs::rtt::Error::ControlBlockNotFound) => { - if try_index < NUM_RETRIES { - log::trace!( - "Could not attach because the target's RTT control block isn't initialized (yet). retrying" - ); - } else { - log::error!("Max number of RTT attach retries exceeded."); - return Err(anyhow!(probe_rs::rtt::Error::ControlBlockNotFound)); - } - } - Err(e) => { - return Err(anyhow!(e)); - } - } - } - - // this block is only executed when rtt was successfully attached before - let mut rtt = rtt_res.expect("unreachable"); - for ch in rtt.up_channels().iter() { - log::debug!( - "up channel {}: {:?}, buffer size {} bytes", - ch.number(), - ch.name(), - ch.buffer_size() - ); - } - for ch in rtt.down_channels().iter() { - log::debug!( - "down channel {}: {:?}, buffer size {} bytes", - ch.number(), - ch.name(), - ch.buffer_size() - ); - } - - Ok(rtt) -} - -fn get_rtt_main_from(elf: &ElfFile) -> anyhow::Result<(Option, u32)> { - let mut rtt = None; - let mut main = None; - - for symbol in elf.symbols() { - let name = match symbol.name() { - Ok(name) => name, - Err(_) => continue, - }; - - match name { - "main" => main = Some(symbol.address() as u32 & !THUMB_BIT), - "_SEGGER_RTT" => rtt = Some(symbol.address() as u32), - _ => {} - } - } - - Ok((rtt, main.ok_or_else(|| anyhow!("`main` symbol not found"))?)) -} diff --git a/examples/tests/tests/l2cap.rs b/examples/tests/tests/ble_l2cap_peripheral.rs similarity index 60% rename from examples/tests/tests/l2cap.rs rename to examples/tests/tests/ble_l2cap_peripheral.rs index 54a56485..92ee43be 100644 --- a/examples/tests/tests/l2cap.rs +++ b/examples/tests/tests/ble_l2cap_peripheral.rs @@ -1,19 +1,21 @@ -use probe_rs::flashing::Format; +use futures::future::select; use std::time::Duration; use tokio::select; -use tokio::sync::oneshot; -use trouble_example_tests::{probe, serial}; +use trouble_example_tests::{probe, serial, TestContext}; use trouble_host::prelude::*; -#[tokio::test(flavor = "multi_thread")] -async fn l2cap_peripheral_nrf52() { +#[tokio::test] +async fn ble_l2cap_peripheral_nrf52() { let _ = pretty_env_logger::try_init(); let fw = std::fs::read("bins/nrf-sdc/ble_l2cap_peripheral").unwrap(); - let firmware = probe::Firmware { - data: fw, - format: Format::Elf, - }; - run_l2cap_peripheral_test(&[("target", "nrf52"), ("board", "microbit")], firmware).await; + let firmware = probe::Firmware { data: fw }; + let local = tokio::task::LocalSet::new(); + local + .run_until(run_l2cap_peripheral_test( + &[("target", "nrf52"), ("board", "microbit")], + firmware, + )) + .await; } /* @@ -30,28 +32,20 @@ async fn l2cap_peripheral_esp32() { */ async fn run_l2cap_peripheral_test(labels: &[(&str, &str)], firmware: probe::Firmware) { - let adapters = serial::find_controllers(); - let central = adapters[0].clone(); - let config = std::env::var("PROBE_CONFIG").unwrap(); - log::info!("Using probe config {}", config); - let config = serde_json::from_str(&config).unwrap(); - - let selector = probe::init(config); - let target = selector.select(labels).expect("no suitable probe found"); - log::info!("Found target {:?}", target); + let ctx = TestContext::new(); + let central = ctx.serial_adapters[0].clone(); - let (cancel_tx, cancel_rx) = oneshot::channel(); + let dut = ctx.find_dut(labels).unwrap(); // Flash the binary to the target - let runner = target.flash(firmware).unwrap(); + let token = dut.token(); // Spawn a runner for the target - let peripheral = tokio::task::spawn(async move { runner.run(cancel_rx).await }); - //let peripheral = tokio::task::spawn(async move {}); + let peripheral = tokio::task::spawn_local(dut.run(firmware)); // Run the central in the test using the serial adapter to verify let peripheral_address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]); - let central_fut = async { + let central = tokio::task::spawn_local(async move { let controller_central = serial::create_controller(¢ral).await; let mut resources: HostResources = HostResources::new(PacketQos::None); let (stack, _peripheral, mut central, mut runner) = @@ -89,7 +83,7 @@ async fn run_l2cap_peripheral_test(labels: &[(&str, &str)], firmware: probe::Fir assert_eq!(rx, [i; PAYLOAD_LEN]); } log::info!("[central] data received"); - cancel_tx.send(()).unwrap(); + token.cancel(); break; } Ok(()) @@ -97,30 +91,13 @@ async fn run_l2cap_peripheral_test(labels: &[(&str, &str)], firmware: probe::Fir r } } - }; + }); - match tokio::time::timeout(Duration::from_secs(30), async { tokio::join!(central_fut, peripheral) }).await { - Ok(result) => match result { - (Err(e1), Err(e2)) => { - println!("Central error: {:?}", e1); - println!("Peripheral error: {:?}", e2); - panic!(); - } - (Err(e), _) => { - println!("Central error: {:?}", e); - panic!(); - } - (_, Err(e)) => { - println!("Peripheral error: {:?}", e); - panic!(); - } - _ => { - println!("Test completed successfully"); - } - }, - Err(e) => { - println!("Test timed out: {:?}", e); - panic!(); - } - } + tokio::time::timeout(Duration::from_secs(60), select(peripheral, central)) + .await + .map_err(|_| { + println!("Test timed out"); + assert!(false); + }) + .unwrap(); }