diff options
author | Edward O'Callaghan <quasisec@google.com> | 2020-02-18 14:38:08 +1100 |
---|---|---|
committer | Edward O'Callaghan <quasisec@chromium.org> | 2020-02-24 09:15:00 +0000 |
commit | 0f510a7458e0efe95534667bba122b4ab67b26c1 (patch) | |
tree | 19032bf855ce51d207b494e1c0ed54e7642c69a7 /util/flashrom_tester/src/tester.rs | |
parent | 7a7fee1695bc3ea9df4a9a058a1805210328d691 (diff) | |
download | flashrom-0f510a7458e0efe95534667bba122b4ab67b26c1.tar.gz flashrom-0f510a7458e0efe95534667bba122b4ab67b26c1.tar.bz2 flashrom-0f510a7458e0efe95534667bba122b4ab67b26c1.zip |
util/flashrom_tester: Upstream E2E testing framework
The following is a E2E tester for a specific chip/chipset
combo. The tester itself is completely self-contained and
allows the user to specify which tests they wish to preform.
Supported tests include:
- chip-name
- read
- write
- erase
- wp-locking
Change-Id: Ic2905a76cad90b1546b9328d668bf8abbf8aed44
Signed-off-by: Edward O'Callaghan <quasisec@google.com>
Reviewed-on: https://review.coreboot.org/c/flashrom/+/38951
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: David Hendricks <david.hendricks@gmail.com>
Diffstat (limited to 'util/flashrom_tester/src/tester.rs')
-rw-r--r-- | util/flashrom_tester/src/tester.rs | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/util/flashrom_tester/src/tester.rs b/util/flashrom_tester/src/tester.rs new file mode 100644 index 00000000..fbef2016 --- /dev/null +++ b/util/flashrom_tester/src/tester.rs @@ -0,0 +1,636 @@ +// +// 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::{FlashChip, Flashrom, FlashromCmd}; +use serde_json::json; +use std::mem::MaybeUninit; +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 FlashromCmd, + 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 FlashromCmd) -> Result<Self, String> { + 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)?, + 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"); + flashrom::read(&out.cmd, &out.original_flash_contents)?; + flashrom::verify(&out.cmd, &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 { + flashrom::verify(&self.cmd, &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<(), String> { + self.wp.set_hw(false)?.set_sw(false)?; + flashrom::write(&self.cmd, &self.original_flash_contents) + } + + /// Attempt to erase the flash. + pub fn erase(&self) -> Result<(), String> { + flashrom::erase(self.cmd) + } + + /// 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<(), String> { + flashrom::verify(self.cmd, contents_path) + } +} + +impl Drop for TestEnv<'_> { + 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 FlashromCmd, +} + +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 FlashromCmd) -> Result<Self, String> { + 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, + }) + } + + /// Get the actual hardware write protect state. + fn get_hw(cmd: &FlashromCmd) -> Result<bool, String> { + if cmd.fc.can_control_hw_wp() { + super::utils::get_hardware_wp() + } else { + Ok(false) + } + } + + /// Get the actual software write protect state. + fn get_sw(cmd: &FlashromCmd) -> Result<bool, String> { + flashrom::wp_status(cmd, true) + } +} + +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.fc.can_control_hw_wp() + } + + /// Set the software write protect. + pub fn set_sw(&mut self, enable: bool) -> Result<&mut Self, String> { + info!("request={}, current={}", enable, self.current.1); + if self.current.1 != enable { + flashrom::wp_toggle(self.cmd, /* 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.cmd.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<(), String> { + /// # let cmd: flashrom::FlashromCmd = unimplemented!(); + /// let wp = flashrom_tester::tester::WriteProtectState::from_hardware(&cmd)?; + /// { + /// 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, + } + } + + 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 + ) + })?; + } + flashrom::wp_toggle(self.cmd, /* en= */ sw).map_err(|e| { + format!( + "Failed to {}able software write protect: {}", + enable_str(sw), + e + ) + })?; + } + + assert!( + self.cmd.fc.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: &FlashromCmd, + ts: TS, +) -> 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 { + 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)); + } +} |