Edward O'Callaghan has uploaded this change for review.

View Change

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>
---
A util/flashrom_tester/.gitignore
A util/flashrom_tester/Cargo.toml
A util/flashrom_tester/build.rs
A util/flashrom_tester/flashrom/Cargo.toml
A util/flashrom_tester/flashrom/src/cmd.rs
A util/flashrom_tester/flashrom/src/lib.rs
A util/flashrom_tester/flashrom_remote.sh
A util/flashrom_tester/src/cros_sysinfo.rs
A util/flashrom_tester/src/lib.rs
A util/flashrom_tester/src/logger.rs
A util/flashrom_tester/src/main.rs
A util/flashrom_tester/src/rand_util.rs
A util/flashrom_tester/src/tester.rs
A util/flashrom_tester/src/tests.rs
A util/flashrom_tester/src/types.rs
A util/flashrom_tester/src/utils.rs
16 files changed, 2,680 insertions(+), 0 deletions(-)

git pull ssh://review.coreboot.org:29418/flashrom refs/changes/51/38951/1
diff --git a/util/flashrom_tester/.gitignore b/util/flashrom_tester/.gitignore
new file mode 100644
index 0000000..1e7caa9
--- /dev/null
+++ b/util/flashrom_tester/.gitignore
@@ -0,0 +1,2 @@
+Cargo.lock
+target/
diff --git a/util/flashrom_tester/Cargo.toml b/util/flashrom_tester/Cargo.toml
new file mode 100644
index 0000000..50f2c4a
--- /dev/null
+++ b/util/flashrom_tester/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "flashrom_tester"
+version = "1.6.0"
+authors = ["Edward O'Callaghan <quasisec@chromium.org>",
+ "Peter Marheine <pmarheine@chromium.org>"]
+edition = "2018"
+build = "build.rs"
+
+[lib]
+name = "flashrom_tester"
+
+[[bin]]
+name = "flashrom_tester"
+required-features = ["cli"]
+
+[dependencies]
+built = { version = "0.3", default-features = false, features = ["serialized_time", "serialized_version"] }
+chrono = { version = "0.4", optional = true }
+clap = { version = "2.33", default-features = false, optional = true }
+flashrom = { path = "flashrom/" }
+log = { version = "0.4", features = ["std"] }
+rand = "0.6.4"
+serde_json = "1"
+sys-info = "0.5.7"
+
+[build-dependencies]
+built = { version = "0.3", default-features = false, features = ["serialized_time", "serialized_version"] }
+
+[features]
+# Features required to build the CLI binary but not the library
+cli = ["chrono", "clap"]
+default = ["cli"]
diff --git a/util/flashrom_tester/build.rs b/util/flashrom_tester/build.rs
new file mode 100644
index 0000000..3800c17
--- /dev/null
+++ b/util/flashrom_tester/build.rs
@@ -0,0 +1,5 @@
+extern crate built;
+
+fn main() {
+ built::write_built_file().expect("Failed to acquire build-time information");
+}
diff --git a/util/flashrom_tester/flashrom/Cargo.toml b/util/flashrom_tester/flashrom/Cargo.toml
new file mode 100644
index 0000000..27216cb
--- /dev/null
+++ b/util/flashrom_tester/flashrom/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "flashrom"
+version = "1.0.0"
+authors = ["Edward O'Callaghan <quasisec@chromium.org>",
+ "Peter Marheine <pmarheine@chromium.org>"]
+edition = "2018"
+
+[dependencies]
+log = "0.4"
\ No newline at end of file
diff --git a/util/flashrom_tester/flashrom/src/cmd.rs b/util/flashrom_tester/flashrom/src/cmd.rs
new file mode 100644
index 0000000..3fd2ac0
--- /dev/null
+++ b/util/flashrom_tester/flashrom/src/cmd.rs
@@ -0,0 +1,355 @@
+//
+// 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 crate::{FlashChip, FlashromError, FlashromOpt};
+
+use std::process::Command;
+
+#[derive(PartialEq, Debug)]
+pub struct FlashromCmd {
+ pub path: String,
+ pub fc: FlashChip,
+}
+
+/// Attempt to determine the Flash size given stdout from `flashrom --flash-size`
+fn flashrom_extract_size(stdout: &str) -> Result<i64, FlashromError> {
+ // Search for the last line of output that contains only digits, assuming
+ // that's the actual size. flashrom sadly tends to write additional messages
+ // to stdout.
+ match stdout
+ .lines()
+ .filter(|line| line.chars().all(|c| c.is_ascii_digit()))
+ .last()
+ .map(str::parse::<i64>)
+ {
+ None => return Err("Found no purely-numeric lines in flashrom output".into()),
+ Some(Err(e)) => {
+ return Err(format!(
+ "Failed to parse flashrom size output as integer: {}",
+ e
+ ))
+ }
+ Some(Ok(sz)) => Ok(sz),
+ }
+}
+
+impl crate::Flashrom for FlashromCmd {
+ fn get_size(&self) -> Result<i64, FlashromError> {
+ let (stdout, _) = flashrom_dispatch(self.path.as_str(), &["--flash-size"], self.fc)?;
+ let sz = String::from_utf8_lossy(&stdout);
+
+ flashrom_extract_size(&sz)
+ }
+
+ fn dispatch(&self, fropt: FlashromOpt) -> Result<(Vec<u8>, Vec<u8>), FlashromError> {
+ let params = flashrom_decode_opts(fropt);
+ flashrom_dispatch(self.path.as_str(), &params, self.fc)
+ }
+}
+
+fn flashrom_decode_opts(opts: FlashromOpt) -> Vec<String> {
+ let mut params = Vec::<String>::new();
+
+ // ------------ WARNING !!! ------------
+ // each param must NOT contain spaces!
+ // -------------------------------------
+
+ // wp_opt
+ if opts.wp_opt.range.is_some() {
+ let (x0, x1) = opts.wp_opt.range.unwrap();
+ params.push("--wp-range".to_string());
+ params.push(hex_string(x0));
+ params.push(hex_string(x1));
+ }
+ if opts.wp_opt.status {
+ params.push("--wp-status".to_string());
+ } else if opts.wp_opt.list {
+ params.push("--wp-list".to_string());
+ } else if opts.wp_opt.enable {
+ params.push("--wp-enable".to_string());
+ } else if opts.wp_opt.disable {
+ params.push("--wp-disable".to_string());
+ }
+
+ // io_opt
+ if opts.io_opt.read.is_some() {
+ params.push("-r".to_string());
+ params.push(opts.io_opt.read.unwrap().to_string());
+ } else if opts.io_opt.write.is_some() {
+ params.push("-w".to_string());
+ params.push(opts.io_opt.write.unwrap().to_string());
+ } else if opts.io_opt.verify.is_some() {
+ params.push("-v".to_string());
+ params.push(opts.io_opt.verify.unwrap().to_string());
+ } else if opts.io_opt.erase {
+ params.push("-E".to_string());
+ }
+
+ // misc_opt
+ if opts.layout.is_some() {
+ params.push("-l".to_string());
+ params.push(opts.layout.unwrap().to_string());
+ }
+ if opts.image.is_some() {
+ params.push("-i".to_string());
+ params.push(opts.image.unwrap().to_string());
+ }
+
+ if opts.flash_name {
+ params.push("--flash-name".to_string());
+ }
+ if opts.ignore_fmap {
+ params.push("--ignore-fmap".to_string());
+ }
+ if opts.verbose {
+ params.push("-V".to_string());
+ }
+
+ params
+}
+
+fn flashrom_dispatch<S: AsRef<str>>(
+ path: &str,
+ params: &[S],
+ fc: FlashChip,
+) -> Result<(Vec<u8>, Vec<u8>), FlashromError> {
+ // from man page:
+ // ' -p, --programmer <name>[:parameter[,parameter[,parameter]]] '
+ let mut args: Vec<&str> = vec!["-p", FlashChip::to(fc)];
+ args.extend(params.iter().map(S::as_ref));
+
+ info!("flashrom_dispatch() running: {} {:?}", path, args);
+
+ let output = match Command::new(path).args(&args).output() {
+ Ok(x) => x,
+ Err(e) => return Err(format!("Failed to run flashrom: {}", e)),
+ };
+ if !output.status.success() {
+ // There is two cases on failure;
+ // i. ) A bad exit code,
+ // ii.) A SIG killed us.
+ match output.status.code() {
+ Some(code) => {
+ return Err(format!(
+ "{}\nExited with error code: {}",
+ String::from_utf8_lossy(&output.stderr),
+ code
+ ));
+ }
+ None => return Err("Process terminated by a signal".into()),
+ }
+ }
+
+ Ok((output.stdout, output.stderr))
+}
+
+pub fn dut_ctrl_toggle_wp(en: bool) -> Result<(Vec<u8>, Vec<u8>), FlashromError> {
+ let args = if en {
+ ["fw_wp_en:off", "fw_wp:on"]
+ } else {
+ ["fw_wp_en:on", "fw_wp:off"]
+ };
+ dut_ctrl(&args)
+}
+
+pub fn dut_ctrl_servo_type() -> Result<(Vec<u8>, Vec<u8>), FlashromError> {
+ let args = ["servo_type"];
+ dut_ctrl(&args)
+}
+
+fn dut_ctrl(args: &[&str]) -> Result<(Vec<u8>, Vec<u8>), FlashromError> {
+ let output = match Command::new("dut-control").args(args).output() {
+ Ok(x) => x,
+ Err(e) => return Err(format!("Failed to run dut-control: {}", e)),
+ };
+ if !output.status.success() {
+ // There is two cases on failure;
+ // i. ) A bad exit code,
+ // ii.) A SIG killed us.
+ match output.status.code() {
+ Some(code) => {
+ return Err(format!("Exited with error code: {}", code).into());
+ }
+ None => return Err("Process terminated by a signal".into()),
+ }
+ }
+
+ Ok((output.stdout, output.stderr))
+}
+
+fn hex_string(v: i64) -> String {
+ format!("{:#08X}", v).to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::flashrom_decode_opts;
+ use crate::{FlashromOpt, IOOpt, WPOpt};
+
+ #[test]
+ fn decode_wp_opt() {
+ fn test_wp_opt(wpo: WPOpt, expected: &[&str]) {
+ assert_eq!(
+ flashrom_decode_opts(FlashromOpt {
+ wp_opt: wpo,
+ ..Default::default()
+ }),
+ expected
+ );
+ }
+
+ test_wp_opt(Default::default(), &[]);
+ test_wp_opt(
+ WPOpt {
+ range: Some((0, 1234)),
+ status: true,
+ ..Default::default()
+ },
+ &["--wp-range", "0x000000", "0x0004D2", "--wp-status"],
+ );
+ test_wp_opt(
+ WPOpt {
+ list: true,
+ ..Default::default()
+ },
+ &["--wp-list"],
+ );
+ test_wp_opt(
+ WPOpt {
+ enable: true,
+ ..Default::default()
+ },
+ &["--wp-enable"],
+ );
+ test_wp_opt(
+ WPOpt {
+ disable: true,
+ ..Default::default()
+ },
+ &["--wp-disable"],
+ );
+ }
+
+ #[test]
+ fn decode_io_opt() {
+ fn test_io_opt(opts: IOOpt, expected: &[&str]) {
+ assert_eq!(
+ flashrom_decode_opts(FlashromOpt {
+ io_opt: opts,
+ ..Default::default()
+ }),
+ expected
+ );
+ }
+
+ test_io_opt(
+ IOOpt {
+ read: Some("foo.bin"),
+ ..Default::default()
+ },
+ &["-r", "foo.bin"],
+ );
+ test_io_opt(
+ IOOpt {
+ write: Some("bar.bin"),
+ ..Default::default()
+ },
+ &["-w", "bar.bin"],
+ );
+ test_io_opt(
+ IOOpt {
+ verify: Some("/tmp/baz.bin"),
+ ..Default::default()
+ },
+ &["-v", "/tmp/baz.bin"],
+ );
+ test_io_opt(
+ IOOpt {
+ erase: true,
+ ..Default::default()
+ },
+ &["-E"],
+ );
+ }
+
+ #[test]
+ fn decode_misc() {
+ //use Default::default;
+ assert_eq!(
+ flashrom_decode_opts(FlashromOpt {
+ layout: Some("TestLayout"),
+ ..Default::default()
+ }),
+ &["-l", "TestLayout"]
+ );
+
+ assert_eq!(
+ flashrom_decode_opts(FlashromOpt {
+ image: Some("TestImage"),
+ ..Default::default()
+ }),
+ &["-i", "TestImage"]
+ );
+
+ assert_eq!(
+ flashrom_decode_opts(FlashromOpt {
+ flash_name: true,
+ ignore_fmap: true,
+ verbose: true,
+ ..Default::default()
+ }),
+ &["--flash-name", "--ignore-fmap", "-V"]
+ );
+ }
+
+ #[test]
+ fn flashrom_extract_size() {
+ use super::flashrom_extract_size;
+
+ assert_eq!(
+ flashrom_extract_size(
+ "coreboot table found at 0x7cc13000.\n\
+ Found chipset \"Intel Braswell\". Enabling flash write... OK.\n\
+ 8388608\n"
+ ),
+ Ok(8388608)
+ );
+
+ assert_eq!(
+ flashrom_extract_size("There was a catastrophic error."),
+ Err("Found no purely-numeric lines in flashrom output".into())
+ );
+ }
+}
diff --git a/util/flashrom_tester/flashrom/src/lib.rs b/util/flashrom_tester/flashrom/src/lib.rs
new file mode 100644
index 0000000..734e3ff
--- /dev/null
+++ b/util/flashrom_tester/flashrom/src/lib.rs
@@ -0,0 +1,381 @@
+//
+// 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.
+//
+
+#[macro_use]
+extern crate log;
+
+mod cmd;
+
+pub use cmd::{dut_ctrl_toggle_wp, FlashromCmd};
+
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub enum FlashChip {
+ EC,
+ HOST,
+ SERVO,
+ DEDIPROG,
+}
+
+impl FlashChip {
+ pub fn from(s: &str) -> Result<FlashChip, &str> {
+ let r = match s {
+ "ec" => Ok(FlashChip::EC),
+ "host" => Ok(FlashChip::HOST),
+ "servo" => Ok(FlashChip::SERVO),
+ "dediprog" => Ok(FlashChip::DEDIPROG),
+ _ => Err("cannot convert str to enum"),
+ };
+ return r;
+ }
+ pub fn to(fc: FlashChip) -> &'static str {
+ let r = match fc {
+ FlashChip::EC => "ec",
+ FlashChip::HOST => "host",
+ FlashChip::SERVO => "ft2231_spi:type=servo-v2",
+ FlashChip::DEDIPROG => "dediprog",
+ };
+ return r;
+ }
+
+ /// Return whether the hardware write protect signal can be controlled.
+ ///
+ /// Servo and dediprog adapters are assumed to always have hardware write protect
+ /// disabled.
+ pub fn can_control_hw_wp(&self) -> bool {
+ match self {
+ FlashChip::HOST | FlashChip::EC => true,
+ FlashChip::SERVO | FlashChip::DEDIPROG => false,
+ }
+ }
+}
+
+pub type FlashromError = String;
+
+#[derive(Default)]
+pub struct FlashromOpt<'a> {
+ pub wp_opt: WPOpt,
+ pub io_opt: IOOpt<'a>,
+
+ pub layout: Option<&'a str>, // -l <file>
+ pub image: Option<&'a str>, // -i <name>
+
+ pub flash_name: bool, // --flash-name
+ pub ignore_fmap: bool, // --ignore-fmap
+ pub verbose: bool, // -V
+}
+
+#[derive(Default)]
+pub struct WPOpt {
+ pub range: Option<(i64, i64)>, // --wp-range x0 x1
+ pub status: bool, // --wp-status
+ pub list: bool, // --wp-list
+ pub enable: bool, // --wp-enable
+ pub disable: bool, // --wp-disable
+}
+
+#[derive(Default)]
+pub struct IOOpt<'a> {
+ pub read: Option<&'a str>, // -r <file>
+ pub write: Option<&'a str>, // -w <file>
+ pub verify: Option<&'a str>, // -v <file>
+ pub erase: bool, // -E
+}
+
+pub trait Flashrom {
+ fn get_size(&self) -> Result<i64, FlashromError>;
+ fn dispatch(&self, fropt: FlashromOpt) -> Result<(Vec<u8>, Vec<u8>), FlashromError>;
+}
+
+pub fn name(cmd: &cmd::FlashromCmd) -> Result<(String, String), FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ ..Default::default()
+ },
+
+ flash_name: true,
+
+ ..Default::default()
+ };
+
+ let (stdout, stderr) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ let eoutput = String::from_utf8_lossy(stderr.as_slice());
+ debug!("name()'stdout: {:#?}.", output);
+ debug!("name()'stderr: {:#?}.", eoutput);
+
+ match extract_flash_name(&output) {
+ None => Err("Didn't find chip vendor/name in flashrom output".into()),
+ Some((vendor, name)) => Ok((vendor.into(), name.into())),
+ }
+}
+
+/// Get a flash vendor and name from the first matching line of flashrom output.
+///
+/// The target line looks like 'vendor="foo" name="bar"', as output by flashrom --flash-name.
+/// This is usually the last line of output.
+fn extract_flash_name(stdout: &str) -> Option<(&str, &str)> {
+ for line in stdout.lines() {
+ if !line.starts_with("vendor=\"") {
+ continue;
+ }
+
+ let tail = line.trim_start_matches("vendor=\"");
+ let mut split = tail.splitn(2, "\" name=\"");
+ let vendor = split.next();
+ let name = split.next().map(|s| s.trim_end_matches('"'));
+
+ match (vendor, name) {
+ (Some(v), Some(n)) => return Some((v, n)),
+ _ => continue,
+ }
+ }
+ None
+}
+
+pub struct ROMWriteSpecifics<'a> {
+ pub layout_file: Option<&'a str>,
+ pub write_file: Option<&'a str>,
+ pub name_file: Option<&'a str>,
+}
+
+pub fn write_file_with_layout(
+ cmd: &cmd::FlashromCmd,
+ rws: &ROMWriteSpecifics,
+) -> Result<bool, FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ write: rws.write_file,
+ ..Default::default()
+ },
+
+ layout: rws.layout_file,
+ image: rws.name_file,
+
+ ignore_fmap: true,
+
+ ..Default::default()
+ };
+
+ let (stdout, stderr) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ let eoutput = String::from_utf8_lossy(stderr.as_slice());
+ debug!("write_file_with_layout()'stdout:\n{}.", output);
+ debug!("write_file_with_layout()'stderr:\n{}.", eoutput);
+ Ok(true)
+}
+
+pub fn wp_range(
+ cmd: &cmd::FlashromCmd,
+ range: (i64, i64),
+ wp_enable: bool,
+) -> Result<bool, FlashromError> {
+ let opts = FlashromOpt {
+ wp_opt: WPOpt {
+ range: Some(range),
+ enable: wp_enable,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, stderr) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ let eoutput = String::from_utf8_lossy(stderr.as_slice());
+ debug!("wp_range()'stdout:\n{}.", output);
+ debug!("wp_range()'stderr:\n{}.", eoutput);
+ Ok(true)
+}
+
+pub fn wp_list(cmd: &cmd::FlashromCmd) -> Result<String, FlashromError> {
+ let opts = FlashromOpt {
+ wp_opt: WPOpt {
+ list: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ if output.len() == 0 {
+ return Err(
+ "wp_list isn't supported on platforms using the Linux kernel SPI driver wp_list".into(),
+ );
+ }
+ Ok(output.to_string())
+}
+
+pub fn wp_status(cmd: &cmd::FlashromCmd, en: bool) -> Result<bool, FlashromError> {
+ let status = if en { "en" } else { "dis" };
+ info!("See if chip write protect is {}abled", status);
+
+ let opts = FlashromOpt {
+ wp_opt: WPOpt {
+ status: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+
+ debug!("wp_status():\n{}", output);
+
+ let s = std::format!("write protect is {}abled", status);
+ Ok(output.contains(&s))
+}
+
+pub fn wp_toggle(cmd: &cmd::FlashromCmd, en: bool) -> Result<bool, FlashromError> {
+ let status = if en { "en" } else { "dis" };
+
+ // For MTD, --wp-range and --wp-enable must be used simultaneously.
+ let range = if en {
+ let rom_sz: i64 = cmd.get_size()?;
+ Some((0, rom_sz)) // (start, len)
+ } else {
+ None
+ };
+
+ let opts = FlashromOpt {
+ wp_opt: WPOpt {
+ range: range,
+ enable: en,
+ disable: !en,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, stderr) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ let eoutput = String::from_utf8_lossy(stderr.as_slice());
+
+ debug!("wp_toggle()'stdout:\n{}.", output);
+ debug!("wp_toggle()'stderr:\n{}.", eoutput);
+
+ match wp_status(&cmd, true) {
+ Ok(_ret) => {
+ info!("Successfully {}abled write-protect", status);
+ Ok(true)
+ }
+ Err(e) => Err(format!("Cannot {}able write-protect: {}", status, e)),
+ }
+}
+
+pub fn read(cmd: &cmd::FlashromCmd, path: &str) -> Result<(), FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ read: Some(path),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ debug!("read():\n{}", output);
+ Ok(())
+}
+
+pub fn write(cmd: &cmd::FlashromCmd, path: &str) -> Result<(), FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ write: Some(path),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ debug!("write():\n{}", output);
+ Ok(())
+}
+
+pub fn verify(cmd: &cmd::FlashromCmd, path: &str) -> Result<(), FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ verify: Some(path),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ debug!("verify():\n{}", output);
+ Ok(())
+}
+
+pub fn erase(cmd: &cmd::FlashromCmd) -> Result<(), FlashromError> {
+ let opts = FlashromOpt {
+ io_opt: IOOpt {
+ erase: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let (stdout, _) = cmd.dispatch(opts)?;
+ let output = String::from_utf8_lossy(stdout.as_slice());
+ debug!("erase():\n{}", output);
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn extract_flash_name() {
+ use super::extract_flash_name;
+
+ assert_eq!(
+ extract_flash_name(
+ "coreboot table found at 0x7cc13000\n\
+ Found chipset \"Intel Braswell\". Enabling flash write... OK.\n\
+ vendor=\"Winbond\" name=\"W25Q64DW\"\n"
+ ),
+ Some(("Winbond", "W25Q64DW"))
+ );
+
+ assert_eq!(
+ extract_flash_name(
+ "vendor name is TEST\n\
+ Something failed!"
+ ),
+ None
+ )
+ }
+}
diff --git a/util/flashrom_tester/flashrom_remote.sh b/util/flashrom_tester/flashrom_remote.sh
new file mode 100755
index 0000000..8dba943
--- /dev/null
+++ b/util/flashrom_tester/flashrom_remote.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+/usr/bin/ssh localhost -p 60024 -C /usr/sbin/flashrom "$@"
diff --git a/util/flashrom_tester/src/cros_sysinfo.rs b/util/flashrom_tester/src/cros_sysinfo.rs
new file mode 100644
index 0000000..ddb0802
--- /dev/null
+++ b/util/flashrom_tester/src/cros_sysinfo.rs
@@ -0,0 +1,80 @@
+//
+// 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 std::ffi::OsStr;
+use std::fmt::Debug;
+use std::io::Result as IoResult;
+use std::process::{Command, Stdio};
+
+use super::utils;
+
+fn dmidecode_dispatch<S: AsRef<OsStr>>(args: &[S]) -> IoResult<String> {
+ let output = Command::new("/usr/sbin/dmidecode")
+ .args(args)
+ .stdin(Stdio::null())
+ .output()?;
+
+ if !output.status.success() {
+ return Err(utils::translate_command_error(&output));
+ }
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
+}
+
+pub fn system_info() -> IoResult<String> {
+ dmidecode_dispatch(&["-q", "-t1"])
+}
+
+pub fn bios_info() -> IoResult<String> {
+ dmidecode_dispatch(&["-q", "-t0"])
+}
+
+pub fn eventlog_list() -> Result<String, std::io::Error> {
+ mosys_dispatch(&["eventlog", "list"])
+}
+
+fn mosys_dispatch<S: AsRef<OsStr> + Debug>(args: &[S]) -> IoResult<String> {
+ info!("mosys_dispatch() running: /usr/sbin/mosys {:?}", args);
+
+ let output = Command::new("/usr/sbin/mosys")
+ .args(args)
+ .stdin(Stdio::null())
+ .output()?;
+ if !output.status.success() {
+ return Err(utils::translate_command_error(&output));
+ }
+
+ let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
+ Ok(stdout)
+}
diff --git a/util/flashrom_tester/src/lib.rs b/util/flashrom_tester/src/lib.rs
new file mode 100644
index 0000000..d8f1cb6
--- /dev/null
+++ b/util/flashrom_tester/src/lib.rs
@@ -0,0 +1,46 @@
+//
+// 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.
+//
+
+#[macro_use]
+extern crate log;
+
+#[macro_use]
+pub mod types;
+
+pub mod cros_sysinfo;
+pub mod rand_util;
+pub mod tester;
+pub mod tests;
+pub mod utils;
diff --git a/util/flashrom_tester/src/logger.rs b/util/flashrom_tester/src/logger.rs
new file mode 100644
index 0000000..e1c00f5
--- /dev/null
+++ b/util/flashrom_tester/src/logger.rs
@@ -0,0 +1,172 @@
+//
+// 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 flashrom_tester::types;
+use std::io::Write;
+use std::path::PathBuf;
+use std::sync::Mutex;
+
+struct Logger<W: Write + Send> {
+ level: log::LevelFilter,
+ target: LogTarget<W>,
+}
+
+enum LogTarget<W>
+where
+ W: Write,
+{
+ Terminal,
+ Write(Mutex<W>),
+}
+
+impl<W: Write + Send> log::Log for Logger<W> {
+ fn enabled(&self, metadata: &log::Metadata) -> bool {
+ metadata.level() <= self.level
+ }
+
+ fn log(&self, record: &log::Record) {
+ fn log_internal<W: Write>(mut w: W, record: &log::Record) -> std::io::Result<()> {
+ let now = chrono::Local::now();
+ write!(w, "{}{} ", types::MAGENTA, now.format("%Y-%m-%dT%H:%M:%S"))?;
+ write!(
+ w,
+ "{}[ {} ]{} ",
+ types::YELLOW,
+ record.level(),
+ types::RESET
+ )?;
+ writeln!(w, "{}", record.args())
+ }
+
+ // Write errors deliberately ignored
+ let _ = match self.target {
+ LogTarget::Terminal => {
+ let stdout = std::io::stdout();
+ let mut lock = stdout.lock();
+ log_internal(&mut lock, record)
+ }
+ LogTarget::Write(ref mutex) => {
+ let mut lock = mutex.lock().unwrap();
+ log_internal(&mut *lock, record)
+ }
+ };
+ }
+
+ fn flush(&self) {
+ // Flush errors deliberately ignored
+ let _ = match self.target {
+ LogTarget::Terminal => std::io::stdout().flush(),
+ LogTarget::Write(ref w) => w.lock().unwrap().flush(),
+ };
+ }
+}
+
+pub fn init(to_file: Option<PathBuf>, debug: bool) {
+ let mut logger = Logger {
+ level: log::LevelFilter::Info,
+ target: LogTarget::Terminal,
+ };
+
+ if debug {
+ logger.level = log::LevelFilter::Debug;
+ }
+ if let Some(path) = to_file {
+ logger.target = LogTarget::Write(Mutex::new(
+ std::fs::File::create(path).expect("Unable to open log file for writing"),
+ ));
+ }
+
+ log::set_max_level(logger.level);
+ log::set_boxed_logger(Box::new(logger)).unwrap();
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{LogTarget, Logger};
+ use log::{Level, LevelFilter, Log, Record};
+ use std::sync::Mutex;
+
+ fn run_records(records: &[Record]) -> String {
+ let mut buf = Vec::<u8>::new();
+ {
+ let lock = Mutex::new(&mut buf);
+ let logger = Logger {
+ level: LevelFilter::Info,
+ target: LogTarget::Write(lock),
+ };
+
+ for record in records {
+ if logger.enabled(record.metadata()) {
+ logger.log(&record);
+ }
+ }
+ }
+ String::from_utf8(buf).unwrap()
+ }
+
+ /// Log messages have the expected format
+ #[test]
+ fn format() {
+ let buf = run_records(&[Record::builder()
+ .args(format_args!("Test message at INFO"))
+ .level(Level::Info)
+ .build()]);
+
+ assert_eq!(&buf[..5], "\x1b[35m");
+ // Time is difficult to test, assume it's formatted okay
+ assert_eq!(
+ &buf[24..],
+ " \x1b[33m[ INFO ]\x1b[0m Test message at INFO\n"
+ );
+ }
+
+ #[test]
+ fn level_filter() {
+ let buf = run_records(&[
+ Record::builder()
+ .args(format_args!("Test message at DEBUG"))
+ .level(Level::Debug)
+ .build(),
+ Record::builder()
+ .args(format_args!("Hello, world!"))
+ .level(Level::Error)
+ .build(),
+ ]);
+
+ // There is one line because the Debug record wasn't written.
+ println!("{}", buf);
+ assert_eq!(buf.lines().count(), 1);
+ }
+}
diff --git a/util/flashrom_tester/src/main.rs b/util/flashrom_tester/src/main.rs
new file mode 100644
index 0000000..1cc525e
--- /dev/null
+++ b/util/flashrom_tester/src/main.rs
@@ -0,0 +1,143 @@
+//
+// 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.
+//
+
+#[macro_use]
+extern crate log;
+
+mod logger;
+
+use clap::{App, Arg};
+use flashrom::FlashChip;
+use flashrom_tester::{tester, tests};
+use std::path::PathBuf;
+
+pub mod built_info {
+ include!(concat!(env!("OUT_DIR"), "/built.rs"));
+}
+
+fn main() {
+ let matches = App::new("flashrom_tester")
+ .long_version(&*format!(
+ "{}-{}\n\
+ Target: {}\n\
+ Profile: {}\n\
+ Features: {:?}\n\
+ Build time: {}\n\
+ Compiler: {}",
+ built_info::PKG_VERSION,
+ option_env!("VCSID").unwrap_or("<unknown>"),
+ built_info::TARGET,
+ built_info::PROFILE,
+ built_info::FEATURES,
+ built_info::BUILT_TIME_UTC,
+ built_info::RUSTC_VERSION,
+ ))
+ .arg(Arg::with_name("flashrom_binary").required(true))
+ .arg(
+ Arg::with_name("ccd_target_type")
+ .required(true)
+ .possible_values(&["host", "ec", "servo"]),
+ )
+ .arg(
+ Arg::with_name("print-layout")
+ .short("l")
+ .long("print-layout")
+ .help("Print the layout file's contents before running tests"),
+ )
+ .arg(
+ Arg::with_name("log-file")
+ .short("o")
+ .long("log-file")
+ .takes_value(true)
+ .help("Write logs to a file rather than stdout"),
+ )
+ .arg(
+ Arg::with_name("log_debug")
+ .short("d")
+ .long("debug")
+ .help("Write detailed logs, for debugging"),
+ )
+ .arg(
+ Arg::with_name("output-format")
+ .short("f")
+ .long("output-format")
+ .help("Set the test report format")
+ .takes_value(true)
+ .case_insensitive(true)
+ .possible_values(&["pretty", "json"])
+ .default_value("pretty"),
+ )
+ .arg(
+ Arg::with_name("test_name")
+ .multiple(true)
+ .help("Names of individual tests to run (run all if unspecified)"),
+ )
+ .get_matches();
+
+ logger::init(
+ matches.value_of_os("log-file").map(PathBuf::from),
+ matches.is_present("log_debug"),
+ );
+ debug!("Args parsed and logging initialized OK");
+
+ let flashrom_path = matches
+ .value_of("flashrom_binary")
+ .expect("flashrom_binary should be required");
+ let ccd_type = FlashChip::from(
+ matches
+ .value_of("ccd_target_type")
+ .expect("ccd_target_type should be required"),
+ )
+ .expect("ccd_target_type should admit only known types");
+
+ let print_layout = matches.is_present("print-layout");
+ let output_format = matches
+ .value_of("output-format")
+ .expect("output-format should have a default value")
+ .parse::<tester::OutputFormat>()
+ .expect("output-format is not a parseable OutputFormat");
+ let test_names = matches.values_of("test_name");
+
+ if let Err(e) = tests::generic(
+ flashrom_path,
+ ccd_type,
+ print_layout,
+ output_format,
+ test_names,
+ ) {
+ eprintln!("Failed to run tests: {:?}", e);
+ std::process::exit(1);
+ }
+}
diff --git a/util/flashrom_tester/src/rand_util.rs b/util/flashrom_tester/src/rand_util.rs
new file mode 100644
index 0000000..51411d0
--- /dev/null
+++ b/util/flashrom_tester/src/rand_util.rs
@@ -0,0 +1,81 @@
+//
+// 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 std::fs::File;
+use std::io::prelude::*;
+use std::io::BufWriter;
+
+use rand::prelude::*;
+
+pub fn gen_rand_testdata(path: &str, size: usize) -> std::io::Result<()> {
+ let mut buf = BufWriter::new(File::create(path)?);
+
+ let mut a: Vec<u8> = Vec::with_capacity(size);
+ // Pad out array to be filled in by Rng::fill().
+ a.resize(size, 0b0);
+ thread_rng().fill(a.as_mut_slice());
+
+ buf.write_all(a.as_slice())?;
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn gen_rand_testdata() {
+ use super::gen_rand_testdata;
+
+ let path0 = "/tmp/idk_test00";
+ let path1 = "/tmp/idk_test01";
+ let sz = 1024;
+
+ gen_rand_testdata(path0, sz).unwrap();
+ gen_rand_testdata(path1, sz).unwrap();
+
+ let mut buf0 = Vec::new();
+ let mut buf1 = Vec::new();
+
+ let mut f = File::open(path0).unwrap();
+ let mut g = File::open(path1).unwrap();
+
+ f.read_to_end(&mut buf0).unwrap();
+ g.read_to_end(&mut buf1).unwrap();
+
+ assert_ne!(buf0, buf1);
+ }
+}
diff --git a/util/flashrom_tester/src/tester.rs b/util/flashrom_tester/src/tester.rs
new file mode 100644
index 0000000..fbef201
--- /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));
+ }
+}
diff --git a/util/flashrom_tester/src/tests.rs b/util/flashrom_tester/src/tests.rs
new file mode 100644
index 0000000..dd75689
--- /dev/null
+++ b/util/flashrom_tester/src/tests.rs
@@ -0,0 +1,385 @@
+//
+// 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::cros_sysinfo;
+use super::tester::{self, OutputFormat, TestCase, TestEnv, TestResult};
+use super::utils::{self, LayoutNames};
+use flashrom::{FlashChip, Flashrom, FlashromCmd};
+use std::collections::{HashMap, HashSet};
+use std::fs::File;
+use std::io::{BufRead, Write};
+
+const LAYOUT_FILE: &'static str = "/tmp/layout.file";
+
+/// Iterate over tests, yielding only those tests with names matching filter_names.
+///
+/// If filter_names is None, all tests will be run. None is distinct from Some(∅);
+// Some(∅) runs no tests.
+///
+/// Name comparisons are performed in lower-case: values in filter_names must be
+/// converted to lowercase specifically.
+///
+/// When an entry in filter_names matches a test, it is removed from that set.
+/// This allows the caller to determine if any entries in the original set failed
+/// to match any test, which may be user error.
+fn filter_tests<'n, 't: 'n, T: TestCase>(
+ tests: &'t [T],
+ filter_names: &'n mut Option<HashSet<String>>,
+) -> impl 'n + Iterator<Item = &'t T> {
+ tests.iter().filter(move |test| match filter_names {
+ // Accept all tests if no names are given
+ None => true,
+ Some(ref mut filter_names) => {
+ // Pop a match to the test name from the filter set, retaining the test
+ // if there was a match.
+ filter_names.remove(&test.get_name().to_lowercase())
+ }
+ })
+}
+
+/// Run tests.
+///
+/// Only returns an Error if there was an internal error; test failures are Ok.
+///
+/// test_names is the case-insensitive names of tests to run; if None, then all
+/// tests are run. Provided names that don't match any known test will be logged
+/// as a warning.
+pub fn generic<'a, TN: Iterator<Item = &'a str>>(
+ path: &str,
+ fc: FlashChip,
+ print_layout: bool,
+ output_format: OutputFormat,
+ test_names: Option<TN>,
+) -> Result<(), Box<dyn std::error::Error>> {
+ let p = path.to_string();
+ let cmd = FlashromCmd { path: p, fc };
+
+ utils::ac_power_warning();
+
+ info!("Calculate ROM partition sizes & Create the layout file.");
+ let rom_sz: i64 = cmd.get_size()?;
+ let layout_sizes = utils::get_layout_sizes(rom_sz)?;
+ {
+ let mut f = File::create(LAYOUT_FILE)?;
+ let mut buf: Vec<u8> = vec![];
+ utils::construct_layout_file(&mut buf, &layout_sizes)?;
+
+ f.write_all(&buf)?;
+ if print_layout {
+ info!(
+ "Dumping layout file as requested:\n{}",
+ String::from_utf8_lossy(&buf)
+ );
+ }
+ }
+
+ info!(
+ "Record crossystem information.\n{}",
+ utils::collect_crosssystem()?
+ );
+
+ // Register tests to run:
+ let tests: &[&dyn TestCase] = &[
+ &("Get_device_name", get_device_name_test),
+ &("Coreboot_ELOG_sanity", elog_sanity_test),
+ &("Host_is_ChromeOS", host_is_chrome_test),
+ &("Toggle_WP", wp_toggle_test),
+ &("Erase_and_Write", erase_write_test),
+ &("Fail_to_verify", verify_fail_test),
+ &("Lock", lock_test),
+ &("Lock_top_quad", partial_lock_test(LayoutNames::TopQuad)),
+ &(
+ "Lock_bottom_quad",
+ partial_lock_test(LayoutNames::BottomQuad),
+ ),
+ &(
+ "Lock_bottom_half",
+ partial_lock_test(LayoutNames::BottomHalf),
+ ),
+ &("Lock_top_half", partial_lock_test(LayoutNames::TopHalf)),
+ ];
+
+ // Limit the tests to only those requested, unless none are requested
+ // in which case all tests are included.
+ let mut filter_names: Option<HashSet<String>> = if let Some(names) = test_names {
+ Some(names.map(|s| s.to_lowercase()).collect())
+ } else {
+ None
+ };
+ let tests = filter_tests(tests, &mut filter_names);
+
+ // ------------------------.
+ // Run all the tests and collate the findings:
+ let results = tester::run_all_tests(fc, &cmd, tests);
+
+ // Any leftover filtered names were specified to be run but don't exist
+ for leftover in filter_names.iter().flatten() {
+ warn!("No test matches filter name \"{}\"", leftover);
+ }
+
+ let chip_name = flashrom::name(&cmd)
+ .map(|x| format!("vendor=\"{}\" name=\"{}\"", x.0, x.1))
+ .unwrap_or("<Unknown chip>".into());
+ let os_rel = sys_info::os_release().unwrap_or("<Unknown OS>".to_string());
+ let system_info = cros_sysinfo::system_info().unwrap_or("<Unknown System>".to_string());
+ let bios_info = cros_sysinfo::bios_info().unwrap_or("<Unknown BIOS>".to_string());
+
+ let meta_data = tester::ReportMetaData {
+ chip_name: chip_name,
+ os_release: os_rel,
+ system_info: system_info,
+ bios_info: bios_info,
+ };
+ tester::collate_all_test_runs(&results, meta_data, output_format);
+ Ok(())
+}
+
+fn get_device_name_test(env: &mut TestEnv) -> TestResult {
+ // Success means we got something back, which is good enough.
+ flashrom::name(env.cmd)?;
+ Ok(())
+}
+
+fn wp_toggle_test(env: &mut TestEnv) -> TestResult {
+ // NOTE: This is not strictly a 'test' as it is allowed to fail on some platforms.
+ // However, we will warn when it does fail.
+ // List the write-protected regions of flash.
+ match flashrom::wp_list(env.cmd) {
+ Ok(list_str) => info!("\n{}", list_str),
+ Err(e) => warn!("{}", e),
+ };
+ // Fails if unable to set either one
+ env.wp.set_hw(false)?;
+ env.wp.set_sw(false)?;
+ Ok(())
+}
+
+fn erase_write_test(env: &mut TestEnv) -> TestResult {
+ if !env.is_golden() {
+ info!("Memory has been modified; reflashing to ensure erasure can be detected");
+ env.ensure_golden()?;
+ }
+
+ // With write protect enabled erase should fail.
+ env.wp.set_sw(true)?.set_hw(true)?;
+ if env.erase().is_ok() {
+ info!("Flashrom returned Ok but this may be incorrect; verifying");
+ if !env.is_golden() {
+ return Err("Hardware write protect asserted however can still erase!".into());
+ }
+ info!("Erase claimed to succeed but verify is Ok; assume erase failed");
+ }
+
+ // With write protect disabled erase should succeed.
+ env.wp.set_hw(false)?.set_sw(false)?;
+ env.erase()?;
+ if env.is_golden() {
+ return Err("Successful erase didn't modify memory".into());
+ }
+
+ Ok(())
+}
+
+fn lock_test(env: &mut TestEnv) -> TestResult {
+ if !env.wp.can_control_hw_wp() {
+ return Err("Lock test requires ability to control hardware write protect".into());
+ }
+
+ env.wp.set_hw(false)?.set_sw(true)?;
+ // Toggling software WP off should work when hardware is off.
+ // Then enable again for another go.
+ env.wp.push().set_sw(false)?;
+
+ env.wp.set_hw(true)?;
+ // Clearing should fail when hardware is enabled
+ if env.wp.set_sw(false).is_ok() {
+ return Err("Software WP was reset despite hardware WP being enabled".into());
+ }
+ Ok(())
+}
+
+fn elog_sanity_test(env: &mut TestEnv) -> TestResult {
+ // Check that the elog contains *something*, as an indication that Coreboot
+ // is actually able to write to the Flash. Because this invokes mosys on the
+ // host, it doesn't make sense to run for other chips.
+ if env.chip_type() != FlashChip::HOST {
+ info!("Skipping ELOG sanity check for non-host chip");
+ return Ok(());
+ }
+ // mosys reads the flash, it should be back in the golden state
+ env.ensure_golden()?;
+ // Output is one event per line, drop empty lines in the interest of being defensive.
+ let event_count = cros_sysinfo::eventlog_list()?
+ .lines()
+ .filter(|l| !l.is_empty())
+ .count();
+
+ if event_count == 0 {
+ Err("ELOG contained no events".into())
+ } else {
+ Ok(())
+ }
+}
+
+fn host_is_chrome_test(_env: &mut TestEnv) -> TestResult {
+ let release_info = if let Ok(f) = File::open("/etc/os-release") {
+ let buf = std::io::BufReader::new(f);
+ parse_os_release(buf.lines().flatten())
+ } else {
+ info!("Unable to read /etc/os-release to probe system information");
+ HashMap::new()
+ };
+
+ match release_info.get("ID") {
+ Some(id) if id == "chromeos" || id == "chromiumos" => Ok(()),
+ oid => {
+ let id = match oid {
+ Some(s) => s,
+ None => "UNKNOWN",
+ };
+ Err(format!(
+ "Test host os-release \"{}\" should be but is not chromeos",
+ id
+ )
+ .into())
+ }
+ }
+}
+
+fn partial_lock_test(section: LayoutNames) -> impl Fn(&mut TestEnv) -> TestResult {
+ move |env: &mut TestEnv| {
+ // Need a clean image for verification
+ env.ensure_golden()?;
+
+ let (name, start, len) = utils::layout_section(env.layout(), section);
+ // Disable software WP so we can do range protection, but hardware WP
+ // must remain enabled for (most) range protection to do anything.
+ env.wp.set_hw(false)?.set_sw(false)?;
+ flashrom::wp_range(env.cmd, (start, len), true)?;
+ env.wp.set_hw(true)?;
+
+ let rws = flashrom::ROMWriteSpecifics {
+ layout_file: Some(LAYOUT_FILE),
+ write_file: Some(env.random_data_file()),
+ name_file: Some(name),
+ };
+ if flashrom::write_file_with_layout(env.cmd, &rws).is_ok() {
+ return Err(
+ "Section should be locked, should not have been overwritable with random data"
+ .into(),
+ );
+ }
+ if !env.is_golden() {
+ return Err("Section didn't lock, has been overwritten with random data!".into());
+ }
+ Ok(())
+ }
+}
+
+fn verify_fail_test(env: &mut TestEnv) -> TestResult {
+ // Comparing the flash contents to random data says they're not the same.
+ match env.verify(env.random_data_file()) {
+ Ok(_) => Err("Verification says flash is full of random data".into()),
+ Err(_) => Ok(()),
+ }
+}
+
+/// Ad-hoc parsing of os-release(5); mostly according to the spec,
+/// but ignores quotes and escaping.
+fn parse_os_release<I: IntoIterator<Item = String>>(lines: I) -> HashMap<String, String> {
+ fn parse_line(line: String) -> Option<(String, String)> {
+ if line.is_empty() || line.starts_with('#') {
+ return None;
+ }
+
+ let delimiter = match line.find('=') {
+ Some(idx) => idx,
+ None => {
+ warn!("os-release entry seems malformed: {:?}", line);
+ return None;
+ }
+ };
+ Some((
+ line[..delimiter].to_owned(),
+ line[delimiter + 1..].to_owned(),
+ ))
+ }
+
+ lines.into_iter().filter_map(parse_line).collect()
+}
+
+#[test]
+fn test_parse_os_release() {
+ let lines = [
+ "BUILD_ID=12516.0.0",
+ "# this line is a comment followed by an empty line",
+ "",
+ "ID_LIKE=chromiumos",
+ "ID=chromeos",
+ "VERSION=79",
+ "EMPTY_VALUE=",
+ ];
+ let map = parse_os_release(lines.iter().map(|&s| s.to_owned()));
+
+ fn get<'a, 'b>(m: &'a HashMap<String, String>, k: &'b str) -> Option<&'a str> {
+ m.get(k).map(|s| s.as_ref())
+ }
+
+ assert_eq!(get(&map, "ID"), Some("chromeos"));
+ assert_eq!(get(&map, "BUILD_ID"), Some("12516.0.0"));
+ assert_eq!(get(&map, "EMPTY_VALUE"), Some(""));
+ assert_eq!(get(&map, ""), None);
+}
+
+#[test]
+fn test_name_filter() {
+ let test_one = ("Test One", |_: &mut TestEnv| Ok(()));
+ let test_two = ("Test Two", |_: &mut TestEnv| Ok(()));
+ let tests: &[&dyn TestCase] = &[&test_one, &test_two];
+
+ let mut names = None;
+ // All tests pass through
+ assert_eq!(filter_tests(tests, &mut names).count(), 2);
+
+ names = Some(["test two"].iter().map(|s| s.to_string()).collect());
+ // Filtered out test one
+ assert_eq!(filter_tests(tests, &mut names).count(), 1);
+
+ names = Some(["test three"].iter().map(|s| s.to_string()).collect());
+ // No tests emitted
+ assert_eq!(filter_tests(tests, &mut names).count(), 0);
+ // Name got left behind because no test matched it
+ assert_eq!(names.unwrap().len(), 1);
+}
diff --git a/util/flashrom_tester/src/types.rs b/util/flashrom_tester/src/types.rs
new file mode 100644
index 0000000..b22ded2
--- /dev/null
+++ b/util/flashrom_tester/src/types.rs
@@ -0,0 +1,53 @@
+//
+// 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.
+//
+
+pub const BOLD: &str = "\x1b[1m";
+
+pub const RESET: &str = "\x1b[0m";
+pub const MAGENTA: &str = "\x1b[35m";
+pub const YELLOW: &str = "\x1b[33m";
+pub const GREEN: &str = "\x1b[92m";
+pub const RED: &str = "\x1b[31m";
+
+macro_rules! style_dbg {
+ ($s: expr, $c: expr) => {
+ format!("{}{:?}{}", $c, $s, types::RESET)
+ };
+}
+macro_rules! style {
+ ($s: expr, $c: expr) => {
+ format!("{}{}{}", $c, $s, types::RESET)
+ };
+}
diff --git a/util/flashrom_tester/src/utils.rs b/util/flashrom_tester/src/utils.rs
new file mode 100644
index 0000000..d17480b
--- /dev/null
+++ b/util/flashrom_tester/src/utils.rs
@@ -0,0 +1,298 @@
+//
+// 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 std::io::prelude::*;
+use std::process::Command;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LayoutNames {
+ TopQuad,
+ TopHalf,
+ BottomHalf,
+ BottomQuad,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LayoutSizes {
+ half_sz: i64,
+ quad_sz: i64,
+ rom_top: i64,
+ bottom_half_top: i64,
+ bottom_quad_top: i64,
+ top_quad_bottom: i64,
+}
+
+pub fn get_layout_sizes(rom_sz: i64) -> Result<LayoutSizes, String> {
+ if rom_sz <= 0 {
+ return Err("invalid rom size provided".into());
+ }
+ if rom_sz & (rom_sz - 1) != 0 {
+ return Err("invalid rom size, not a power of 2".into());
+ }
+ Ok(LayoutSizes {
+ half_sz: rom_sz / 2,
+ quad_sz: rom_sz / 4,
+ rom_top: rom_sz - 1,
+ bottom_half_top: (rom_sz / 2) - 1,
+ bottom_quad_top: (rom_sz / 4) - 1,
+ top_quad_bottom: (rom_sz / 4) * 3,
+ })
+}
+
+pub fn layout_section(ls: &LayoutSizes, ln: LayoutNames) -> (&'static str, i64, i64) {
+ match ln {
+ LayoutNames::TopQuad => ("TOP_QUAD", ls.top_quad_bottom, ls.quad_sz),
+ LayoutNames::TopHalf => ("TOP_HALF", ls.half_sz, ls.half_sz),
+ LayoutNames::BottomHalf => ("BOTTOM_HALF", 0, ls.half_sz),
+ LayoutNames::BottomQuad => ("BOTTOM_QUAD", 0, ls.quad_sz),
+ }
+}
+
+pub fn construct_layout_file<F: Write>(mut target: F, ls: &LayoutSizes) -> std::io::Result<()> {
+ writeln!(target, "000000:{:x} BOTTOM_QUAD", ls.bottom_quad_top)?;
+ writeln!(target, "000000:{:x} BOTTOM_HALF", ls.bottom_half_top)?;
+ writeln!(target, "{:x}:{:x} TOP_HALF", ls.half_sz, ls.rom_top)?;
+ writeln!(target, "{:x}:{:x} TOP_QUAD", ls.top_quad_bottom, ls.rom_top)
+}
+
+pub fn toggle_hw_wp(dis: bool) -> Result<(), String> {
+ // The easist way to toggle the harware write-protect is
+ // to {dis}connect the battery (and/or open the WP screw).
+ let s = if dis { "dis" } else { "" };
+ info!("Prompt for hardware WP {}able", s);
+ eprintln!(" > {}connect the battery (and/or open the WP screw)", s);
+ pause();
+ let wp = get_hardware_wp()?;
+ if wp && dis {
+ eprintln!("Hardware write protect is still ENABLED!");
+ return toggle_hw_wp(dis);
+ }
+ if !wp && !dis {
+ eprintln!("Hardware write protect is still DISABLED!");
+ return toggle_hw_wp(dis);
+ }
+ Ok(())
+}
+
+pub fn ac_power_warning() {
+ info!("*****************************");
+ info!("AC power *must be* connected!");
+ info!("*****************************");
+ pause();
+}
+
+fn pause() {
+ let mut stdout = std::io::stdout();
+ // We want the cursor to stay at the end of the line, so we print without a newline
+ // and flush manually.
+ stdout.write(b"Press any key to continue...").unwrap();
+ stdout.flush().unwrap();
+ std::io::stdin().read(&mut [0]).unwrap();
+}
+
+pub fn get_hardware_wp() -> std::result::Result<bool, String> {
+ let (_, wp) = parse_crosssystem(&collect_crosssystem()?)?;
+ Ok(wp)
+}
+
+pub fn collect_crosssystem() -> Result<String, String> {
+ let cmd = match Command::new("crossystem").output() {
+ Ok(x) => x,
+ Err(e) => return Err(format!("Failed to run crossystem: {}", e)),
+ };
+
+ if !cmd.status.success() {
+ return Err(translate_command_error(&cmd).to_string());
+ };
+
+ Ok(String::from_utf8_lossy(&cmd.stdout).into_owned())
+}
+
+fn parse_crosssystem(s: &str) -> Result<(Vec<&str>, bool), &'static str> {
+ // grep -v 'fwid +=' | grep -v 'hwid +='
+ let sysinfo = s
+ .split_terminator("\n")
+ .filter(|s| !s.contains("fwid +=") && !s.contains("hwid +="));
+
+ let state_line = match sysinfo.clone().filter(|s| s.starts_with("wpsw_cur")).next() {
+ None => return Err("No wpsw_cur in system info"),
+ Some(line) => line,
+ };
+ let wp_s_val = state_line
+ .trim_start_matches("wpsw_cur")
+ .trim_start_matches(' ')
+ .trim_start_matches('=')
+ .trim_start_matches(' ')
+ .get(..1)
+ .unwrap()
+ .parse::<u32>();
+
+ match wp_s_val {
+ Ok(v) => {
+ if v == 1 {
+ return Ok((sysinfo.collect(), true));
+ } else if v == 0 {
+ return Ok((sysinfo.collect(), false));
+ } else {
+ return Err("Unknown state value");
+ }
+ }
+ Err(_) => return Err("Cannot parse state value"),
+ }
+}
+
+pub fn translate_command_error(output: &std::process::Output) -> std::io::Error {
+ use std::io::{Error, ErrorKind};
+ // There is two cases on failure;
+ // i. ) A bad exit code,
+ // ii.) A SIG killed us.
+ match output.status.code() {
+ Some(code) => {
+ let e = format!(
+ "{}\nExited with error code: {}",
+ String::from_utf8_lossy(&output.stderr),
+ code
+ );
+ Error::new(ErrorKind::Other, e)
+ }
+ None => Error::new(
+ ErrorKind::Other,
+ "Process terminated by a signal".to_string(),
+ ),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn construct_layout_file() {
+ use super::{construct_layout_file, get_layout_sizes};
+
+ let mut buf = Vec::new();
+ construct_layout_file(
+ &mut buf,
+ &get_layout_sizes(0x10000).expect("64k is a valid chip size"),
+ )
+ .expect("no I/O errors expected");
+
+ assert_eq!(
+ &buf[..],
+ &b"000000:3fff BOTTOM_QUAD\n\
+ 000000:7fff BOTTOM_HALF\n\
+ 8000:ffff TOP_HALF\n\
+ c000:ffff TOP_QUAD\n"[..]
+ );
+ }
+
+ #[test]
+ fn get_layout_sizes() {
+ use super::get_layout_sizes;
+
+ assert_eq!(
+ get_layout_sizes(-128).err(),
+ Some("invalid rom size provided".into())
+ );
+
+ assert_eq!(
+ get_layout_sizes(3 << 20).err(),
+ Some("invalid rom size, not a power of 2".into())
+ );
+
+ assert_eq!(
+ get_layout_sizes(64 << 10).unwrap(),
+ LayoutSizes {
+ half_sz: 0x8000,
+ quad_sz: 0x4000,
+ rom_top: 0xFFFF,
+ bottom_half_top: 0x7FFF,
+ bottom_quad_top: 0x3FFF,
+ top_quad_bottom: 0xC000,
+ }
+ );
+ }
+
+ #[test]
+ fn parse_crosssystem() {
+ use super::parse_crosssystem;
+
+ assert_eq!(
+ parse_crosssystem("This is not the tool you are looking for").err(),
+ Some("No wpsw_cur in system info")
+ );
+
+ assert_eq!(
+ parse_crosssystem("wpsw_cur = ERROR").err(),
+ Some("Cannot parse state value")
+ );
+
+ assert_eq!(
+ parse_crosssystem("wpsw_cur = 3").err(),
+ Some("Unknown state value")
+ );
+
+ assert_eq!(
+ parse_crosssystem("wpsw_cur = 0"),
+ Ok((vec!["wpsw_cur = 0"], false))
+ );
+
+ assert_eq!(
+ parse_crosssystem("wpsw_cur = 1"),
+ Ok((vec!["wpsw_cur = 1"], true))
+ );
+
+ assert_eq!(
+ parse_crosssystem("wpsw_cur=1"),
+ Ok((vec!["wpsw_cur=1"], true))
+ );
+
+ assert_eq!(
+ parse_crosssystem(
+ "fwid += 123wpsw_cur\n\
+ hwid += aaaaa\n\
+ wpsw_boot = 0 # [RO/int]\n\
+ wpsw_cur = 1 # [RO/int]\n"
+ ),
+ Ok((
+ vec![
+ "wpsw_boot = 0 # [RO/int]",
+ "wpsw_cur = 1 # [RO/int]"
+ ],
+ true
+ ))
+ );
+ }
+}

To view, visit change 38951. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: flashrom
Gerrit-Branch: master
Gerrit-Change-Id: Ic2905a76cad90b1546b9328d668bf8abbf8aed44
Gerrit-Change-Number: 38951
Gerrit-PatchSet: 1
Gerrit-Owner: Edward O'Callaghan <quasisec@chromium.org>
Gerrit-MessageType: newchange