use std::fs;
use rstest::rstest;
const CRATE: &str = "test-flip-link-app";
const FILES: [&str; 4] = ["crash", "exception", "hello", "panic"];
const TARGET: &str = "thumbv7em-none-eabi";
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[rstest]
#[case::normal(true)]
#[case::custom_linkerscript(false)]
fn should_link_example_firmware(#[case] default_features: bool) {
cargo::check_flip_link();
let cmd = cargo::build_example_firmware(default_features);
cmd.success();
}
#[test]
fn should_verify_memory_layout() -> Result<()> {
cargo::check_flip_link();
cargo::build_example_firmware(true).success();
for elf_path in elf::paths() {
let elf = fs::read(&elf_path)?;
let object = object::File::parse(&*elf)?;
let [bss, data, uninit, vector_table] = elf::get_sections(&object);
let initial_sp = elf::compute_initial_sp(&vector_table)?;
let bounds = elf::get_bounds(&[data, bss, uninit])?;
assert!(initial_sp <= *bounds.start(),);
}
Ok(())
}
mod cargo {
use std::process::Command;
use assert_cmd::{assert::Assert, prelude::*};
use super::*;
#[must_use]
pub(crate) fn build_example_firmware(default_features: bool) -> Assert {
let mut firmware_dir = std::env::current_dir().unwrap();
firmware_dir.push(CRATE);
let default_features = match default_features {
false => "--no-default-features",
true => "-v",
};
Command::new("cargo")
.args(["build", "--examples", default_features])
.current_dir(firmware_dir)
.unwrap()
.assert()
}
pub(crate) fn check_flip_link() {
Command::new("which")
.arg("flip-link")
.unwrap()
.assert()
.success();
}
}
mod elf {
use std::{ops::RangeInclusive, path::PathBuf};
use object::{File, Object, ObjectSection, Section};
use super::*;
pub(crate) fn compute_initial_sp(vector_table: &Section) -> Result<u64> {
let data = vector_table.uncompressed_data()?;
let sp = u32::from_le_bytes(data[..4].try_into()?);
Ok(sp as u64)
}
pub(crate) fn get_bounds(sections: &[Section]) -> Result<RangeInclusive<u64>> {
let addresses = sections
.iter()
.flat_map(|sec| [sec.address(), sec.address() + sec.size()])
.collect::<Vec<_>>();
let min = *addresses.iter().min().ok_or("empty iterator".to_string())?;
let max = *addresses.iter().max().ok_or("empty iterator".to_string())?;
Ok(min..=max)
}
pub(crate) fn get_sections<'file>(object: &'file File<'_>) -> [Section<'file, 'file>; 4] {
let get_section = |section_name| object.section_by_name(section_name).expect(section_name);
[
get_section(".bss"),
get_section(".data"),
get_section(".uninit"),
get_section(".vector_table"),
]
}
pub(crate) fn paths() -> Vec<PathBuf> {
FILES
.into_iter()
.map(|file_name| format!("{CRATE}/target/{TARGET}/debug/examples/{file_name}"))
.map(PathBuf::from)
.collect()
}
}