1
0
mirror of https://review.coreboot.org/flashrom.git synced 2025-04-28 07:23:43 +02:00
Evan Benn b41bb5622c flashrom_tester: Add _into_file to function names
Rename Flashrom trait function names to reflect that the data is read
to/from a file provided as an argument.

BUG=None
BRANCH=None
TEST=cargo test; cargo check

Change-Id: I0015c9bf64349a5512dbdb0ef6f3dad38aa2fd8e
Signed-off-by: Evan Benn <evanbenn@chromium.org>
Reviewed-on: https://review.coreboot.org/c/flashrom/+/66956
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Edward O'Callaghan <quasisec@chromium.org>
2022-08-26 04:46:47 +00:00

656 lines
22 KiB
Rust

//
// Copyright 2019, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Alternatively, this software may be distributed under the terms of the
// GNU General Public License ("GPL") version 2 as published by the Free
// Software Foundation.
//
use super::rand_util;
use super::types;
use super::utils::{self, LayoutSizes};
use flashrom::FlashromError;
use flashrom::{FlashChip, Flashrom};
use serde_json::json;
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
// type-signature comes from the return type of lib.rs workers.
type TestError = Box<dyn std::error::Error>;
pub type TestResult = Result<(), TestError>;
pub struct TestEnv<'a> {
chip_type: FlashChip,
/// Flashrom instantiation information.
///
/// Where possible, prefer to use methods on the TestEnv rather than delegating
/// to the raw flashrom functions.
pub cmd: &'a dyn Flashrom,
layout: LayoutSizes,
pub wp: WriteProtectState<'a, 'static>,
/// The path to a file containing the flash contents at test start.
// TODO(pmarheine) migrate this to a PathBuf for clarity
original_flash_contents: String,
/// The path to a file containing flash-sized random data
// TODO(pmarheine) make this a PathBuf too
random_data: String,
}
impl<'a> TestEnv<'a> {
pub fn create(chip_type: FlashChip, cmd: &'a dyn Flashrom) -> Result<Self, FlashromError> {
let rom_sz = cmd.get_size()?;
let out = TestEnv {
chip_type: chip_type,
cmd: cmd,
layout: utils::get_layout_sizes(rom_sz)?,
wp: WriteProtectState::from_hardware(cmd, chip_type)?,
original_flash_contents: "/tmp/flashrom_tester_golden.bin".into(),
random_data: "/tmp/random_content.bin".into(),
};
info!("Stashing golden image for verification/recovery on completion");
out.cmd.read_into_file(&out.original_flash_contents)?;
out.cmd.verify_from_file(&out.original_flash_contents)?;
info!("Generating random flash-sized data");
rand_util::gen_rand_testdata(&out.random_data, rom_sz as usize)
.map_err(|io_err| format!("I/O error writing random data file: {:#}", io_err))?;
Ok(out)
}
pub fn run_test<T: TestCase>(&mut self, test: T) -> TestResult {
let use_dut_control = self.chip_type == FlashChip::SERVO;
if use_dut_control && flashrom::dut_ctrl_toggle_wp(false).is_err() {
error!("failed to dispatch dut_ctrl_toggle_wp()!");
}
let name = test.get_name();
info!("Beginning test: {}", name);
let out = test.run(self);
info!("Completed test: {}; result {:?}", name, out);
if use_dut_control && flashrom::dut_ctrl_toggle_wp(true).is_err() {
error!("failed to dispatch dut_ctrl_toggle_wp()!");
}
out
}
pub fn chip_type(&self) -> FlashChip {
// This field is not public because it should be immutable to tests,
// so this getter enforces that it is copied.
self.chip_type
}
/// Return the path to a file that contains random data and is the same size
/// as the flash chip.
pub fn random_data_file(&self) -> &str {
&self.random_data
}
pub fn layout(&self) -> &LayoutSizes {
&self.layout
}
/// Return true if the current Flash contents are the same as the golden image
/// that was present at the start of testing.
pub fn is_golden(&self) -> bool {
self.cmd
.verify_from_file(&self.original_flash_contents)
.is_ok()
}
/// Do whatever is necessary to make the current Flash contents the same as they
/// were at the start of testing.
pub fn ensure_golden(&mut self) -> Result<(), FlashromError> {
self.wp.set_hw(false)?.set_sw(false)?;
self.cmd.write_from_file(&self.original_flash_contents)?;
Ok(())
}
/// Attempt to erase the flash.
pub fn erase(&self) -> Result<(), FlashromError> {
self.cmd.erase()?;
Ok(())
}
/// Verify that the current Flash contents are the same as the file at the given
/// path.
///
/// Returns Err if they are not the same.
pub fn verify(&self, contents_path: &str) -> Result<(), FlashromError> {
self.cmd.verify_from_file(contents_path)?;
Ok(())
}
}
impl<'a> Drop for TestEnv<'a> {
fn drop(&mut self) {
info!("Verifying flash remains unmodified");
if !self.is_golden() {
warn!("ROM seems to be in a different state at finish; restoring original");
if let Err(e) = self.ensure_golden() {
error!("Failed to write back golden image: {:?}", e);
}
}
}
}
/// RAII handle for setting write protect in either hardware or software.
///
/// Given an instance, the state of either write protect can be modified by calling
/// `set` or `push`. When it goes out of scope, the write protects will be returned
/// to the state they had then it was created.
///
/// The lifetime `'p` on this struct is the parent state it derives from; `'static`
/// implies it is derived from hardware, while anything else is part of a stack
/// created by `push`ing states. An initial state is always static, and the stack
/// forms a lifetime chain `'static -> 'p -> 'p1 -> ... -> 'pn`.
pub struct WriteProtectState<'a, 'p> {
/// The parent state this derives from.
///
/// If it's a root (gotten via `from_hardware`), then this is Hardware and the
/// liveness flag will be reset on drop.
initial: InitialState<'p>,
// Tuples are (hardware, software)
current: (bool, bool),
cmd: &'a dyn Flashrom,
fc: FlashChip,
}
enum InitialState<'p> {
Hardware(bool, bool),
Previous(&'p WriteProtectState<'p, 'p>),
}
impl InitialState<'_> {
fn get_target(&self) -> (bool, bool) {
match self {
InitialState::Hardware(hw, sw) => (*hw, *sw),
InitialState::Previous(s) => s.current,
}
}
}
impl<'a> WriteProtectState<'a, 'static> {
/// Initialize a state from the current state of the hardware.
///
/// Panics if there is already a live state derived from hardware. In such a situation the
/// new state must be derived from the live one, or the live one must be dropped first.
pub fn from_hardware(cmd: &'a dyn Flashrom, fc: FlashChip) -> Result<Self, FlashromError> {
let mut lock = Self::get_liveness_lock()
.lock()
.expect("Somebody panicked during WriteProtectState init from hardware");
if *lock {
drop(lock); // Don't poison the lock
panic!("Attempted to create a new WriteProtectState when one is already live");
}
let hw = Self::get_hw(cmd)?;
let sw = Self::get_sw(cmd)?;
info!("Initial hardware write protect: HW={} SW={}", hw, sw);
*lock = true;
Ok(WriteProtectState {
initial: InitialState::Hardware(hw, sw),
current: (hw, sw),
cmd,
fc,
})
}
/// Get the actual hardware write protect state.
fn get_hw(cmd: &dyn Flashrom) -> Result<bool, String> {
if cmd.can_control_hw_wp() {
super::utils::get_hardware_wp()
} else {
Ok(false)
}
}
/// Get the actual software write protect state.
fn get_sw(cmd: &dyn Flashrom) -> Result<bool, FlashromError> {
let b = cmd.wp_status(true)?;
Ok(b)
}
}
impl<'a, 'p> WriteProtectState<'a, 'p> {
/// Return true if the current programmer supports setting the hardware
/// write protect.
///
/// If false, calls to set_hw() will do nothing.
pub fn can_control_hw_wp(&self) -> bool {
self.cmd.can_control_hw_wp()
}
/// Set the software write protect.
pub fn set_sw(&mut self, enable: bool) -> Result<&mut Self, FlashromError> {
info!("request={}, current={}", enable, self.current.1);
if self.current.1 != enable {
self.cmd.wp_toggle(/* en= */ enable)?;
self.current.1 = enable;
}
Ok(self)
}
/// Set the hardware write protect.
pub fn set_hw(&mut self, enable: bool) -> Result<&mut Self, String> {
if self.current.0 != enable {
if self.can_control_hw_wp() {
super::utils::toggle_hw_wp(/* dis= */ !enable)?;
self.current.0 = enable;
} else if enable {
info!(
"Ignoring attempt to enable hardware WP with {:?} programmer",
self.fc
);
}
}
Ok(self)
}
/// Stack a new write protect state on top of the current one.
///
/// This is useful if you need to temporarily make a change to write protection:
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let cmd: flashrom::FlashromCmd = unimplemented!();
/// let wp = flashrom_tester::tester::WriteProtectState::from_hardware(&cmd, flashrom::FlashChip::SERVO)?;
/// {
/// let mut wp = wp.push();
/// wp.set_sw(false)?;
/// // Do something with software write protect disabled
/// }
/// // Now software write protect returns to its original state, even if
/// // set_sw() failed.
/// # Ok(())
/// # }
/// ```
///
/// This returns a new state which restores the original when it is dropped- the new state
/// refers to the old, so the compiler enforces that states are disposed of in the reverse
/// order of their creation and correctly restore the original state.
pub fn push<'p1>(&'p1 self) -> WriteProtectState<'a, 'p1> {
WriteProtectState {
initial: InitialState::Previous(self),
current: self.current,
cmd: self.cmd,
fc: self.fc,
}
}
fn get_liveness_lock() -> &'static Mutex<bool> {
static INIT: std::sync::Once = std::sync::Once::new();
/// Value becomes true when there is a live WriteProtectState derived `from_hardware`,
/// blocking duplicate initialization.
///
/// This is required because hardware access is not synchronized; it's possible to leave the
/// hardware in an unintended state by creating a state handle from it, modifying the state,
/// creating another handle from the hardware then dropping the first handle- then on drop
/// of the second handle it will restore the state to the modified one rather than the initial.
///
/// This flag ensures that a duplicate root state cannot be created.
///
/// This is a Mutex<bool> rather than AtomicBool because acquiring the flag needs to perform
/// several operations that may themselves fail- acquisitions must be fully synchronized.
static mut LIVE_FROM_HARDWARE: MaybeUninit<Mutex<bool>> = MaybeUninit::uninit();
unsafe {
INIT.call_once(|| {
LIVE_FROM_HARDWARE.as_mut_ptr().write(Mutex::new(false));
});
&*LIVE_FROM_HARDWARE.as_ptr()
}
}
/// Reset the hardware to what it was when this state was created, reporting errors.
///
/// This behaves exactly like allowing a state to go out of scope, but it can return
/// errors from that process rather than panicking.
pub fn close(mut self) -> Result<(), String> {
unsafe {
let out = self.drop_internal();
// We just ran drop, don't do it again
std::mem::forget(self);
out
}
}
/// Internal Drop impl.
///
/// This is unsafe because it effectively consumes self when clearing the
/// liveness lock. Callers must be able to guarantee that self will be forgotten
/// if the state was constructed from hardware in order to uphold the liveness
/// invariant (that only a single state constructed from hardware exists at any
/// time).
unsafe fn drop_internal(&mut self) -> Result<(), String> {
let lock = match self.initial {
InitialState::Hardware(_, _) => Some(
Self::get_liveness_lock()
.lock()
.expect("Somebody panicked during WriteProtectState drop from hardware"),
),
_ => None,
};
let (hw, sw) = self.initial.get_target();
fn enable_str(enable: bool) -> &'static str {
if enable {
"en"
} else {
"dis"
}
}
// Toggle both protects back to their initial states.
// Software first because we can't change it once hardware is enabled.
if sw != self.current.1 {
// Is the hw wp currently enabled?
if self.current.0 {
super::utils::toggle_hw_wp(/* dis= */ true).map_err(|e| {
format!(
"Failed to {}able hardware write protect: {}",
enable_str(false),
e
)
})?;
}
self.cmd.wp_toggle(/* en= */ sw).map_err(|e| {
format!(
"Failed to {}able software write protect: {}",
enable_str(sw),
e
)
})?;
}
assert!(
self.cmd.can_control_hw_wp() || (!self.current.0 && !hw),
"HW WP must be disabled if it cannot be controlled"
);
if hw != self.current.0 {
super::utils::toggle_hw_wp(/* dis= */ !hw).map_err(|e| {
format!(
"Failed to {}able hardware write protect: {}",
enable_str(hw),
e
)
})?;
}
if let Some(mut lock) = lock {
// Initial state was constructed via from_hardware, now we can clear the liveness
// lock since reset is complete.
*lock = false;
}
Ok(())
}
}
impl<'a, 'p> Drop for WriteProtectState<'a, 'p> {
/// Sets both write protects to the state they had when this state was created.
///
/// Panics on error because there is no mechanism to report errors in Drop.
fn drop(&mut self) {
unsafe { self.drop_internal() }.expect("Error while dropping WriteProtectState")
}
}
pub trait TestCase {
fn get_name(&self) -> &str;
fn expected_result(&self) -> TestConclusion;
fn run(&self, env: &mut TestEnv) -> TestResult;
}
impl<S: AsRef<str>, F: Fn(&mut TestEnv) -> TestResult> TestCase for (S, F) {
fn get_name(&self) -> &str {
self.0.as_ref()
}
fn expected_result(&self) -> TestConclusion {
TestConclusion::Pass
}
fn run(&self, env: &mut TestEnv) -> TestResult {
(self.1)(env)
}
}
impl<T: TestCase + ?Sized> TestCase for &T {
fn get_name(&self) -> &str {
(*self).get_name()
}
fn expected_result(&self) -> TestConclusion {
(*self).expected_result()
}
fn run(&self, env: &mut TestEnv) -> TestResult {
(*self).run(env)
}
}
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum TestConclusion {
Pass,
Fail,
UnexpectedPass,
UnexpectedFail,
}
pub struct ReportMetaData {
pub chip_name: String,
pub os_release: String,
pub system_info: String,
pub bios_info: String,
}
fn decode_test_result(res: TestResult, con: TestConclusion) -> (TestConclusion, Option<TestError>) {
use TestConclusion::*;
match (res, con) {
(Ok(_), Fail) => (UnexpectedPass, None),
(Err(e), Pass) => (UnexpectedFail, Some(e)),
_ => (Pass, None),
}
}
pub fn run_all_tests<T, TS>(
chip: FlashChip,
cmd: &dyn Flashrom,
ts: TS,
terminate_flag: Option<&AtomicBool>,
) -> Vec<(String, (TestConclusion, Option<TestError>))>
where
T: TestCase + Copy,
TS: IntoIterator<Item = T>,
{
let mut env = TestEnv::create(chip, cmd).expect("Failed to set up test environment");
let mut results = Vec::new();
for t in ts {
if terminate_flag
.map(|b| b.load(Ordering::Acquire))
.unwrap_or(false)
{
break;
}
let result = decode_test_result(env.run_test(t), t.expected_result());
results.push((t.get_name().into(), result));
}
results
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum OutputFormat {
Pretty,
Json,
}
impl std::str::FromStr for OutputFormat {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use OutputFormat::*;
if s.eq_ignore_ascii_case("pretty") {
Ok(Pretty)
} else if s.eq_ignore_ascii_case("json") {
Ok(Json)
} else {
Err(())
}
}
}
pub fn collate_all_test_runs(
truns: &[(String, (TestConclusion, Option<TestError>))],
meta_data: ReportMetaData,
format: OutputFormat,
) {
match format {
OutputFormat::Pretty => {
println!();
println!(" =============================");
println!(" ===== AVL qual RESULTS ====");
println!(" =============================");
println!();
println!(" %---------------------------%");
println!(" os release: {}", meta_data.os_release);
println!(" chip name: {}", meta_data.chip_name);
println!(" system info: \n{}", meta_data.system_info);
println!(" bios info: \n{}", meta_data.bios_info);
println!(" %---------------------------%");
println!();
for trun in truns.iter() {
let (name, (result, error)) = trun;
if *result != TestConclusion::Pass {
println!(
" {} {}",
style!(format!(" <+> {} test:", name), types::BOLD),
style_dbg!(result, types::RED)
);
match error {
None => {}
Some(e) => info!(" - {} failure details:\n{}", name, e.to_string()),
};
} else {
println!(
" {} {}",
style!(format!(" <+> {} test:", name), types::BOLD),
style_dbg!(result, types::GREEN)
);
}
}
println!();
}
OutputFormat::Json => {
use serde_json::{Map, Value};
let mut all_pass = true;
let mut tests = Map::<String, Value>::new();
for (name, (result, error)) in truns {
let passed = *result == TestConclusion::Pass;
all_pass &= passed;
let error = match error {
Some(e) => Value::String(format!("{:#?}", e)),
None => Value::Null,
};
assert!(
!tests.contains_key(name),
"Found multiple tests named {:?}",
name
);
tests.insert(
name.into(),
json!({
"pass": passed,
"error": error,
}),
);
}
let json = json!({
"pass": all_pass,
"metadata": {
"os_release": meta_data.os_release,
"chip_name": meta_data.chip_name,
"system_info": meta_data.system_info,
"bios_info": meta_data.bios_info,
},
"tests": tests,
});
println!("{:#}", json);
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn decode_test_result() {
use super::decode_test_result;
use super::TestConclusion::*;
let (result, err) = decode_test_result(Ok(()), Pass);
assert_eq!(result, Pass);
assert!(err.is_none());
let (result, err) = decode_test_result(Ok(()), Fail);
assert_eq!(result, UnexpectedPass);
assert!(err.is_none());
let (result, err) = decode_test_result(Err("broken".into()), Pass);
assert_eq!(result, UnexpectedFail);
assert!(err.is_some());
let (result, err) = decode_test_result(Err("broken".into()), Fail);
assert_eq!(result, Pass);
assert!(err.is_none());
}
#[test]
fn output_format_round_trip() {
use super::OutputFormat::{self, *};
assert_eq!(format!("{:?}", Pretty).parse::<OutputFormat>(), Ok(Pretty));
assert_eq!(format!("{:?}", Json).parse::<OutputFormat>(), Ok(Json));
}
}