[test] Add rom_e2e_bootstrap_shutdown

Fixes #14467

Signed-off-by: Alphan Ulusoy <alphan@google.com>
diff --git a/sw/host/opentitanlib/src/uart/console.rs b/sw/host/opentitanlib/src/uart/console.rs
index dabe18e..e6950aa 100644
--- a/sw/host/opentitanlib/src/uart/console.rs
+++ b/sw/host/opentitanlib/src/uart/console.rs
@@ -24,7 +24,7 @@
     pub newline: bool,
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub enum ExitStatus {
     None,
     CtrlC,
diff --git a/sw/host/tests/rom/e2e_bootstrap_entry/src/main.rs b/sw/host/tests/rom/e2e_bootstrap_entry/src/main.rs
index 9ef385d..ae99fb7 100644
--- a/sw/host/tests/rom/e2e_bootstrap_entry/src/main.rs
+++ b/sw/host/tests/rom/e2e_bootstrap_entry/src/main.rs
@@ -12,6 +12,7 @@
 
 use opentitanlib::app::TransportWrapper;
 use opentitanlib::execute_test;
+use opentitanlib::io::spi::Transfer;
 use opentitanlib::spiflash::{
     sfdp, BlockEraseSize, SpiFlash, SupportedAddressModes, WriteGranularity,
 };
@@ -251,6 +252,49 @@
     Ok(())
 }
 
+fn test_bootstrap_shutdown(
+    opts: &Opts,
+    transport: &TransportWrapper,
+    cmd: u8,
+    bfv: &str,
+) -> Result<()> {
+    let _bs = BootstrapTest::start(transport, opts.init.bootstrap.options.reset_delay)?;
+
+    let spi = transport.spi("0")?;
+    let uart = transport.uart("0")?;
+    let mut console = UartConsole {
+        timeout: Some(Duration::new(2, 0)),
+        // `kErrorBootPolicyBadIdentifier` (0142500d) is defined in `error.h`.
+        exit_success: Some(Regex::new(
+            format!("(?s)BFV:{bfv}\r\n.*BFV:0142500d\r\n").as_str(),
+        )?),
+        ..Default::default()
+    };
+    // Send CHIP_ERASE to transition to phase 2.
+    SpiFlash::from_spi(&*spi)?.chip_erase(&*spi)?;
+    // Remove strapping so that chip fails to boot instead of going into bootstrap.
+    transport.remove_pin_strapping("ROM_BOOTSTRAP")?;
+    // SECTOR_ERASE with invalid address to trigger a shutdown.
+    SpiFlash::set_write_enable(&*spi)?;
+    let bad_erase = [cmd, 0xff, 0xff, 0xff];
+    spi.run_transaction(&mut [Transfer::Write(&bad_erase)])?;
+    // We should see the expected BFVs.
+    let result = console.interact(&*uart, None, Some(&mut std::io::stdout()))?;
+    if result != ExitStatus::ExitSuccess {
+        bail!("FAIL: {:?}", result);
+    }
+    // Also verify that the chip is no longer in bootstrap mode.
+    assert!(matches!(
+        SpiFlash::read_sfdp(&*spi)
+            .unwrap_err()
+            .downcast::<sfdp::Error>()
+            .unwrap(),
+        sfdp::Error::WrongHeaderSignature(..)
+    ));
+
+    Ok(())
+}
+
 fn main() -> Result<()> {
     let opts = Opts::from_args();
     opts.init.init_logging();
@@ -261,5 +305,21 @@
     execute_test!(test_jedec_id, &opts, &transport);
     execute_test!(test_sfdp, &opts, &transport);
     execute_test!(test_write_enable_disable, &opts, &transport);
+    // `kErrorBootstrapEraseAddress` (01425303) is defined in `error.h`.
+    execute_test!(
+        test_bootstrap_shutdown,
+        &opts,
+        &transport,
+        SpiFlash::SECTOR_ERASE,
+        "01425303"
+    );
+    // `kErrorBootstrapProgramAddress` (02425303) is defined in `error.h`.
+    execute_test!(
+        test_bootstrap_shutdown,
+        &opts,
+        &transport,
+        SpiFlash::PAGE_PROGRAM,
+        "02425303"
+    );
     Ok(())
 }