mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
test: round-3 coverage on platform shim, ld/aacs error paths, css/mt1959 branches
Targets the files round-1/2 left out of scope: ld/platform/mod.rs (the PlatformDriver default trait method, previously 0%), ld/error.rs Display impl (now 100%), aacs/mod.rs Default + full unlock_bus success path (exposes handshake::tests::DriveEmu/dummy_cert as pub(crate) for reuse), css/mod.rs (DvdUnlocker::default, the disc-key-refusal non-fatal path, drive-challenge/Key2-send failure branches), aacs/handshake.rs (run_cert_handshake's per-cert backoff sleep + retry), and mt1959 mod.rs/variant_a.rs (MODE_B dispatch, a non-transport pass-1 probe sense, WRITE_BUFFER's 24-bit length guard, variant_a's full upload+ verify+double-unlock success path). Total coverage 95.65% -> 96.50% (regions), 95.33% -> 96.61% (lines).
This commit is contained in:
+28
-5
@@ -1635,7 +1635,7 @@ pub fn run_cert_handshake(
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
|
||||
@@ -1649,7 +1649,7 @@ mod tests {
|
||||
|
||||
/// A host cert good enough to reach the SCSI steps (the crypto is exercised
|
||||
/// by the math tests; these tests are about the transport contract).
|
||||
fn dummy_cert() -> crate::HostCert {
|
||||
pub(crate) fn dummy_cert() -> crate::HostCert {
|
||||
crate::HostCert {
|
||||
private_key: [0x11u8; 20],
|
||||
certificate: vec![0u8; 92],
|
||||
@@ -1749,6 +1749,29 @@ mod tests {
|
||||
assert_eq!(err, crate::UnlockError::HandshakeRejected);
|
||||
}
|
||||
|
||||
/// A drive that rejects the FIRST cert (a plain cert-level rejection, not a
|
||||
/// wedge or a dead bus) must roll on to the second cert — and the roll-on
|
||||
/// is gated by the per-cert backoff sleep (`idx > 0`), which fires only on
|
||||
/// the second and later attempts. Pins both: the loop actually retries
|
||||
/// with a second host cert (not just re-reporting the first failure), and
|
||||
/// the backoff genuinely elapses real time (catches deleting the sleep or
|
||||
/// gating it on the wrong condition).
|
||||
#[test]
|
||||
fn a_rejected_first_cert_backs_off_and_tries_the_second() {
|
||||
let mut t = MockTransport::always(Reply::illegal_request());
|
||||
let certs = vec![dummy_cert(), dummy_cert()];
|
||||
let started = std::time::Instant::now();
|
||||
let err = run_cert_handshake(&mut t, &certs).expect_err("both certs rejected");
|
||||
assert_eq!(err, crate::UnlockError::HandshakeRejected);
|
||||
assert!(
|
||||
started.elapsed() >= std::time::Duration::from_millis(900),
|
||||
"must have slept the per-cert backoff before the second attempt"
|
||||
);
|
||||
// Each attempt issues 4 invalidations + 1 AGID alloc (which is where
|
||||
// the rejection fires) = 5 CDBs; two attempts = 10.
|
||||
assert_eq!(t.calls(), 10, "must have actually issued a second attempt");
|
||||
}
|
||||
|
||||
/// Script a full, successful AACS 1.0 mutual auth against the mock.
|
||||
///
|
||||
/// The drive presents a type-0x11 (AACS 2.0) certificate, which the
|
||||
@@ -1895,7 +1918,7 @@ mod tests {
|
||||
/// answer the Volume ID read with a MAC that actually verifies — the only
|
||||
/// way to drive `run_cert_handshake` past the VID gate and into the
|
||||
/// read-data-keys arm with the crate's static mocks unable to.
|
||||
struct DriveEmu {
|
||||
pub(crate) struct DriveEmu {
|
||||
drive_priv: [u8; 20],
|
||||
drive_x: [u8; 20],
|
||||
drive_y: [u8; 20],
|
||||
@@ -1904,11 +1927,11 @@ mod tests {
|
||||
/// When set, format-0x84 (read-data-keys) is served a genuine non-zero
|
||||
/// response instead of the default dead-bus behaviour, letting
|
||||
/// `run_cert_handshake` run all the way to its `Ok` return.
|
||||
serve_data_keys: bool,
|
||||
pub(crate) serve_data_keys: bool,
|
||||
}
|
||||
|
||||
impl DriveEmu {
|
||||
fn new() -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (drive_priv, drive_x, drive_y) = generate_host_key_pair();
|
||||
DriveEmu {
|
||||
drive_priv,
|
||||
|
||||
@@ -140,6 +140,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Default` must delegate to `new()` (there is only one way to build an
|
||||
/// `AacsCert`, so this pins the two constructors never drift apart).
|
||||
#[test]
|
||||
#[allow(clippy::default_constructed_unit_structs)]
|
||||
fn default_matches_new() {
|
||||
let _ = AacsCert::default();
|
||||
let _ = AacsCert::new();
|
||||
}
|
||||
|
||||
/// THE full success path through `Unlocker::unlock_bus` itself: a
|
||||
/// self-consistent AACS 1.0 drive emulator carries auth + VID + data-key
|
||||
/// reads all the way to `Ok(Unlocked { .. })`, so the crate's public entry
|
||||
/// point — not just `run_cert_handshake` underneath it — is proven to wire
|
||||
/// `volume_id` and `read_data_key` into `Unlocked` correctly.
|
||||
#[test]
|
||||
fn unlock_bus_succeeds_end_to_end() {
|
||||
let mut t = handshake::tests::DriveEmu::new();
|
||||
t.serve_data_keys = true;
|
||||
let id = id();
|
||||
let certs = [host_cert()];
|
||||
let ctx = UnlockCtx::new(&id, DiscKind::Aacs, &certs);
|
||||
let unlocked = AacsCert::new()
|
||||
.unlock_bus(&mut t, &ctx)
|
||||
.expect("auth + VID + data-key reads all succeed");
|
||||
assert_eq!(unlocked.vid, Some([0x5Au8; 16]));
|
||||
assert!(unlocked.bus_key.is_some());
|
||||
assert!(
|
||||
!unlocked.drive_unlocked,
|
||||
"cert path never sets drive_unlocked"
|
||||
);
|
||||
}
|
||||
|
||||
fn host_cert() -> crate::HostCert {
|
||||
handshake::tests::dummy_cert()
|
||||
}
|
||||
|
||||
/// With no host certs there is nothing to authenticate with → NoUsableHostCert,
|
||||
/// and the transport is never touched.
|
||||
#[test]
|
||||
|
||||
+108
@@ -1021,6 +1021,15 @@ mod tests {
|
||||
assert_eq!(DvdUnlocker::new().name(), "DVD");
|
||||
}
|
||||
|
||||
/// `Default` must delegate to `new()` — there is only one way to build a
|
||||
/// `DvdUnlocker` (it is a unit struct), so this pins the two never drift.
|
||||
#[test]
|
||||
#[allow(clippy::default_constructed_unit_structs)]
|
||||
fn default_matches_new() {
|
||||
let _ = DvdUnlocker::default();
|
||||
let _ = DvdUnlocker::new();
|
||||
}
|
||||
|
||||
/// DvdUnlocker provides bus removal only — it never provides drive features.
|
||||
#[test]
|
||||
fn dvd_unlocker_provides_no_features() {
|
||||
@@ -1270,6 +1279,70 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Host challenge + Key1 both succeed, but the drive challenge REPORT KEY
|
||||
/// (step 3) is refused. Distinct from `key1_report_failure_fails_the_auth`
|
||||
/// (step 2) and `key2_send_failure_fails_the_auth` (step 4) below — each
|
||||
/// pins a different `css_scsi` call site's `?`.
|
||||
#[test]
|
||||
fn drive_challenge_report_failure_fails_the_auth() {
|
||||
struct FailAtDriveChallenge(FakeDvdDrive);
|
||||
impl ScsiTransport for FailAtDriveChallenge {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> crate::scsi::Result<crate::scsi::ScsiResult> {
|
||||
if cdb[0] == crate::scsi::SCSI_REPORT_KEY && cdb[10] & 0x3F == 0x01 {
|
||||
return Ok(crate::scsi::ScsiResult {
|
||||
status: 0x02,
|
||||
bytes_transferred: 0,
|
||||
sense: [0u8; 32],
|
||||
});
|
||||
}
|
||||
self.0.execute(cdb, dir, data, timeout_ms)
|
||||
}
|
||||
}
|
||||
let mut t = FailAtDriveChallenge(FakeDvdDrive {
|
||||
variant: 4,
|
||||
host_challenge: [0u8; 10],
|
||||
});
|
||||
let e = authenticate_with_agid(&mut t, 0).expect_err("refused drive challenge");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
}
|
||||
|
||||
/// Host challenge, Key1, and the drive challenge all succeed, but the
|
||||
/// Key2 SEND KEY (step 4, the last command in the handshake) is refused.
|
||||
#[test]
|
||||
fn key2_send_failure_fails_the_auth() {
|
||||
struct FailAtKey2Send(FakeDvdDrive);
|
||||
impl ScsiTransport for FailAtKey2Send {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> crate::scsi::Result<crate::scsi::ScsiResult> {
|
||||
if cdb[0] == crate::scsi::SCSI_SEND_KEY && cdb[10] & 0x3F == 0x03 {
|
||||
return Ok(crate::scsi::ScsiResult {
|
||||
status: 0x02,
|
||||
bytes_transferred: 0,
|
||||
sense: [0u8; 32],
|
||||
});
|
||||
}
|
||||
self.0.execute(cdb, dir, data, timeout_ms)
|
||||
}
|
||||
}
|
||||
let mut t = FailAtKey2Send(FakeDvdDrive {
|
||||
variant: 4,
|
||||
host_challenge: [0u8; 10],
|
||||
});
|
||||
let e = authenticate_with_agid(&mut t, 0).expect_err("refused Key2 send");
|
||||
assert!(matches!(e, Error::CssAuthFailed));
|
||||
}
|
||||
|
||||
// ── read_disc_key ────────────────────────────────────────────────────────
|
||||
|
||||
/// The best-effort disc-key REPORT KEY is refused — `read_disc_key` must
|
||||
@@ -1422,6 +1495,41 @@ mod tests {
|
||||
unlock_css_reads(&mut t, 0).expect("css bus-auth + best-effort disc key succeed");
|
||||
}
|
||||
|
||||
/// A drive whose bus-auth succeeds but refuses the best-effort disc-key
|
||||
/// REPORT KEY (CHECK CONDITION). `unlock_css_reads_inner` must swallow
|
||||
/// that failure — the read barrier is already open from bus-auth, so the
|
||||
/// whole unlock must still report `Ok`, not propagate the disc-key error.
|
||||
struct DiscKeyRefusingDrive(FakeDvdDrive);
|
||||
|
||||
impl ScsiTransport for DiscKeyRefusingDrive {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> crate::scsi::Result<crate::scsi::ScsiResult> {
|
||||
if cdb[0] == crate::scsi::SCSI_READ_DISC_STRUCTURE {
|
||||
return Ok(crate::scsi::ScsiResult {
|
||||
status: 0x02,
|
||||
bytes_transferred: 0,
|
||||
sense: [0u8; 32],
|
||||
});
|
||||
}
|
||||
self.0.execute(cdb, dir, data, timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disc_key_refusal_is_non_fatal_to_the_overall_unlock() {
|
||||
let mut t = DiscKeyRefusingDrive(FakeDvdDrive {
|
||||
variant: 5,
|
||||
host_challenge: [0u8; 10],
|
||||
});
|
||||
unlock_css_reads(&mut t, 0)
|
||||
.expect("bus-auth succeeded; a refused best-effort disc-key read must not fail it");
|
||||
}
|
||||
|
||||
/// `crypt_key`'s `key_type == 2` arm (`PERM_VARIANT[1]`) has no
|
||||
/// production caller (only key_type 0, the variant brute-force, and 1,
|
||||
/// the drive-challenge Key2, are ever invoked from `authenticate_with_agid`)
|
||||
|
||||
@@ -136,6 +136,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Display` is what shows up in logs — every variant must render without
|
||||
/// panicking and the SCSI variant must carry the opcode/status through.
|
||||
#[test]
|
||||
fn display_renders_every_variant() {
|
||||
assert_eq!(Error::ProfileParse.to_string(), "drive profile parse error");
|
||||
assert_eq!(Error::UnlockFailed.to_string(), "firmware unlock failed");
|
||||
assert_eq!(
|
||||
Error::SignatureMismatch {
|
||||
expected: [0; 4],
|
||||
got: [1; 4],
|
||||
}
|
||||
.to_string(),
|
||||
"signature mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
Error::Scsi {
|
||||
opcode: 0x3C,
|
||||
status: 0x02,
|
||||
sense: None,
|
||||
}
|
||||
.to_string(),
|
||||
"SCSI error (opcode 0x3c, status 0x02)"
|
||||
);
|
||||
}
|
||||
|
||||
/// `From<ScsiError>` must carry the status AND sense across the seam —
|
||||
/// collapsing either one destroys the transport-vs-rejection distinction the
|
||||
/// whole abort/fall-through split rests on.
|
||||
|
||||
@@ -397,6 +397,15 @@ mod tests {
|
||||
assert_eq!(got, None);
|
||||
}
|
||||
|
||||
/// Public catalog accessors: `profiles()` returns the bundled catalog and
|
||||
/// `profile()` is `profiles().and_then(get)` for a known drive.
|
||||
#[test]
|
||||
fn public_catalog_accessors_find_the_bundled_fixture_drive() {
|
||||
assert!(super::profiles().is_some(), "bundled catalog must parse");
|
||||
let m = super::profile(&known_vid_drive_id()).expect("known fixture drive matches");
|
||||
assert!(m.profile.read_vid_cdb.is_some());
|
||||
}
|
||||
|
||||
/// `unlock_features` on a drive with no matching profile → `NotApplicable`
|
||||
/// (fall through), short-circuiting before any firmware handshake.
|
||||
#[test]
|
||||
@@ -525,6 +534,83 @@ mod tests {
|
||||
assert_eq!(err, UnlockError::Transport);
|
||||
}
|
||||
|
||||
/// A drive-sense (not a dead bus) rejecting the speed probe is a genuine
|
||||
/// calibration miss, not a transport fault — `firmware_unlock` must
|
||||
/// warn-and-continue on the drive's default speed table, still reporting a
|
||||
/// full unlock. The non-transport sibling of
|
||||
/// `transport_fault_during_probe_disc_is_transport_not_a_successful_unlock`.
|
||||
#[test]
|
||||
fn drive_sense_during_probe_disc_still_reports_a_successful_unlock() {
|
||||
let id = known_vid_drive_id();
|
||||
let sig = profile::find_bundled(&id)
|
||||
.expect("profile")
|
||||
.profile
|
||||
.signature;
|
||||
|
||||
let mut resp = vec![0u8; 64];
|
||||
resp[0..4].copy_from_slice(&sig);
|
||||
resp[12..16].copy_from_slice(&[0x4D, 0x4D, 0x6B, 0x76]); // primary marker
|
||||
resp[16..20].copy_from_slice(&[0x4C, 0x62, 0x44, 0x72]); // secondary marker
|
||||
|
||||
struct ProbeSenseDrive {
|
||||
resp: Vec<u8>,
|
||||
}
|
||||
impl ScsiTransport for ProbeSenseDrive {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
_dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> crate::scsi::Result<ScsiResult> {
|
||||
// READ_BUFFER (0x3C) / SUB_CMD_PROBE (0x14) is the speed probe.
|
||||
if cdb.first() == Some(&0x3C) && cdb.get(3) == Some(&0x14) {
|
||||
return Ok(ScsiResult {
|
||||
status: 0x02,
|
||||
bytes_transferred: 0,
|
||||
sense: [0u8; 32],
|
||||
});
|
||||
}
|
||||
let n = self.resp.len().min(data.len());
|
||||
data[..n].copy_from_slice(&self.resp[..n]);
|
||||
Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: n,
|
||||
sense: [0u8; 32],
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut t = ProbeSenseDrive { resp };
|
||||
let u = LibreDrive::new()
|
||||
.unlock_features(&mut t, &ctx(&id))
|
||||
.expect("a calibration miss must not fail the whole unlock");
|
||||
assert!(u.drive_unlocked);
|
||||
}
|
||||
|
||||
/// `unlock_bus` is the same firmware code as `unlock_features` (bus removal
|
||||
/// comes free with the drive unlock) — pins that the trait method actually
|
||||
/// delegates rather than being an accidental no-op stub.
|
||||
#[test]
|
||||
fn unlock_bus_delegates_to_the_same_firmware_unlock() {
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
let id = known_vid_drive_id();
|
||||
let sig = profile::find_bundled(&id)
|
||||
.expect("profile")
|
||||
.profile
|
||||
.signature;
|
||||
|
||||
let mut resp = vec![0u8; 64];
|
||||
resp[0..4].copy_from_slice(&sig);
|
||||
resp[12..16].copy_from_slice(&[0x4D, 0x4D, 0x6B, 0x76]);
|
||||
resp[16..20].copy_from_slice(&[0x4C, 0x62, 0x44, 0x72]);
|
||||
|
||||
let mut t = MockTransport::always(Reply::good(resp));
|
||||
let u = LibreDrive::new()
|
||||
.unlock_bus(&mut t, &ctx(&id))
|
||||
.expect("unlock_bus runs the same firmware handshake");
|
||||
assert!(u.drive_unlocked);
|
||||
}
|
||||
|
||||
/// Catches classifying a dead bus as "not this unlocker's drive": the very
|
||||
/// first unlock command faulting at the transport layer must abort the
|
||||
/// consumer (`Transport`), not fall through to the next unlocker.
|
||||
|
||||
@@ -25,3 +25,41 @@ pub(crate) trait PlatformDriver: Send {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
|
||||
/// A minimal driver that takes every `PlatformDriver` default (`is_unlocked`)
|
||||
/// so the shim's dispatch and the default's `false` are both exercised —
|
||||
/// every real driver (`Mt1959`) overrides `is_unlocked`, so without this the
|
||||
/// default body is dead in coverage even though it is reachable code.
|
||||
struct StubDriver;
|
||||
|
||||
impl PlatformDriver for StubDriver {
|
||||
fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
scsi.execute(&[0], crate::scsi::DataDirection::None, &mut [], 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn probe_disc(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
scsi.execute(&[0], crate::scsi::DataDirection::None, &mut [], 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_unlocked_is_false_and_trait_methods_dispatch() {
|
||||
let mut drv = StubDriver;
|
||||
let mut scsi = MockTransport::always(Reply::good(vec![]));
|
||||
assert!(drv.init(&mut scsi).is_ok());
|
||||
assert!(drv.probe_disc(&mut scsi).is_ok());
|
||||
assert!(drv.is_ready());
|
||||
assert!(!drv.is_unlocked());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,6 +823,49 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A drive SENSE (not a dead bus) during the coarse speed probe is the
|
||||
/// drive's OWN failure, not a transport fault — `run_probe` must report
|
||||
/// `UnlockFailed`, not `Transport`. The sibling of
|
||||
/// `transport_fault_during_pass1_probe_stays_a_transport_failure`, which
|
||||
/// only exercises the `is_transport_failure()` arm.
|
||||
#[test]
|
||||
fn drive_sense_during_pass1_probe_is_unlock_failed_not_transport() {
|
||||
struct SenseAtFirstProbe;
|
||||
impl ScsiTransport for SenseAtFirstProbe {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
_dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> crate::scsi::Result<ScsiResult> {
|
||||
if cdb.first() == Some(&SCSI_READ_BUFFER) && cdb.get(3) == Some(&SUB_CMD_PROBE) {
|
||||
return Ok(ScsiResult {
|
||||
status: 0x02,
|
||||
bytes_transferred: 0,
|
||||
sense: [0u8; 32],
|
||||
});
|
||||
}
|
||||
for b in data.iter_mut() {
|
||||
*b = 0;
|
||||
}
|
||||
Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: data.len(),
|
||||
sense: [0u8; 32],
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut t = SenseAtFirstProbe;
|
||||
let mut mt = Mt1959::new(fixture_profile([0; 4]), false);
|
||||
mt.init_complete = true;
|
||||
let e = mt
|
||||
.run_probe(&mut t)
|
||||
.expect_err("drive-refused pass-1 probe");
|
||||
assert!(!e.is_transport_failure(), "a sense is not a dead bus");
|
||||
assert!(matches!(e, Error::UnlockFailed));
|
||||
}
|
||||
|
||||
/// THE probe pass-1 dead-bus test. A bus that dies during the coarse speed
|
||||
/// probe must keep its transport classification out of `run_probe` so the
|
||||
/// caller (`firmware_unlock`) aborts with `Transport` rather than reporting a
|
||||
@@ -917,6 +960,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A firmware blob larger than WRITE_BUFFER's 24-bit CDB length field
|
||||
/// cannot be uploaded — encoding it would silently disagree with the
|
||||
/// bytes actually sent. Must reject before issuing any CDB.
|
||||
#[test]
|
||||
fn variant_a_refuses_firmware_over_the_24bit_write_buffer_length() {
|
||||
let mut profile = fixture_profile([0; 4]);
|
||||
profile.firmware = vec![0u8; 0x0100_0000]; // one past WRITE_BUFFER_MAX_LEN
|
||||
let mut mt = Mt1959::new(profile, false);
|
||||
let mut t = RecordingTransport { cdbs: Vec::new() };
|
||||
let e = variant_a::load_firmware(&mut mt, &mut t).expect_err("blob too large to encode");
|
||||
assert!(matches!(e, Error::UnlockFailed));
|
||||
assert!(t.cdbs.is_empty(), "no CDB may reach the drive");
|
||||
}
|
||||
|
||||
/// THE full success path: WRITE_BUFFER upload, a fully-good verify read
|
||||
/// (exercising the `Ok(r) if status==0 && bytes_transferred==len` arm no
|
||||
/// other variant_a test reaches), then BOTH `do_unlock` calls succeed —
|
||||
/// the first fatally-gated, the second best-effort. Every other
|
||||
/// variant_a test above deliberately stops short (empty firmware, a
|
||||
/// dead bus, or `do_unlock` failing against `RecordingTransport`'s empty
|
||||
/// response) — this is the only one that reaches `load_firmware`'s final
|
||||
/// `Ok(())`.
|
||||
#[test]
|
||||
fn variant_a_load_firmware_succeeds_end_to_end() {
|
||||
let sig = [0x11, 0x22, 0x33, 0x44];
|
||||
let mut profile = fixture_profile(sig);
|
||||
profile.firmware = vec![0u8; 64];
|
||||
let mut mt = Mt1959::new(profile, false);
|
||||
let unlock_response = build_response(sig, FIRMWARE_ACTIVE_SIG, FIRMWARE_MODE_SIG);
|
||||
let mut t = crate::scsi::mock::MockTransport::scripted(
|
||||
vec![
|
||||
crate::scsi::mock::Reply::good(vec![]), // WRITE_BUFFER upload
|
||||
crate::scsi::mock::Reply::good(vec![0u8; VALIDATE_RESPONSE_SIZE as usize]), // verify read, full
|
||||
crate::scsi::mock::Reply::good(unlock_response.clone()), // do_unlock #1
|
||||
crate::scsi::mock::Reply::good(unlock_response), // do_unlock #2 (best-effort)
|
||||
],
|
||||
crate::scsi::mock::Reply::TransportFault,
|
||||
);
|
||||
variant_a::load_firmware(&mut mt, &mut t).expect("upload + verify + double unlock all ok");
|
||||
assert!(mt.init_complete, "do_unlock #1 must have set init_complete");
|
||||
}
|
||||
|
||||
/// variant_b had no transport-abort test. A firmware-upload STEP that hits a
|
||||
/// dead bus (here the metadata read) must abort via `trace_step`, not be
|
||||
/// swallowed and carried into the unlock retries.
|
||||
@@ -1083,6 +1168,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Same shape as `run_init_exhausts_after_three_generic_failures`, but for
|
||||
/// a MODE_B (`is_variant_b = true`) drive — pins that `run_init`'s mode
|
||||
/// dispatch (`self.mode == MODE_A`) actually reaches `variant_b::load_firmware`
|
||||
/// and not just its `variant_a` sibling, which every other `run_init` test
|
||||
/// above exercises exclusively.
|
||||
#[test]
|
||||
fn run_init_exhausts_after_three_generic_failures_mode_b() {
|
||||
let mut t = crate::scsi::mock::MockTransport::always(crate::scsi::mock::Reply::short(
|
||||
vec![0u8; 64],
|
||||
10,
|
||||
));
|
||||
let mut mt = Mt1959::new(fixture_profile([0; 4]), true);
|
||||
let e = mt.init(&mut t).expect_err("never validates, never uploads");
|
||||
assert!(matches!(e, Error::UnlockFailed));
|
||||
assert!(!e.is_transport_failure());
|
||||
assert_eq!(
|
||||
t.calls(),
|
||||
3,
|
||||
"one do_unlock call per attempt, three attempts, no firmware CDBs"
|
||||
);
|
||||
}
|
||||
|
||||
/// Same generic `do_unlock` failure, but this time the firmware-reload
|
||||
/// fallback itself hits a dead bus. That must abort `run_init`
|
||||
/// immediately (transport classification preserved), not be folded into
|
||||
|
||||
Reference in New Issue
Block a user