use libfreemkv::error::Error;
use libfreemkv::scsi::{DataDirection, ScsiResult, ScsiTransport};
struct MockTransport {
script: Vec<MockOutcome>,
next: usize,
}
#[derive(Clone)]
enum MockOutcome {
Ok {
data: Vec<u8>,
resid: i32,
},
TransportFailure,
ScsiFailure {
status: u8,
sense_key: u8,
},
}
impl MockTransport {
fn new(script: Vec<MockOutcome>) -> Self {
Self { script, next: 0 }
}
}
impl ScsiTransport for MockTransport {
fn execute(
&mut self,
cdb: &[u8],
_direction: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> libfreemkv::error::Result<ScsiResult> {
let i = self.next;
self.next += 1;
let outcome = self
.script
.get(i)
.cloned()
.expect("MockTransport script ran out — test wrote fewer outcomes than calls");
match outcome {
MockOutcome::Ok { data: d, resid } => {
let n = d.len().min(data.len());
data[..n].copy_from_slice(&d[..n]);
let bytes = (data.len() as i32).saturating_sub(resid).max(0) as usize;
Ok(ScsiResult {
status: 0,
bytes_transferred: bytes,
sense: [0u8; 32],
})
}
MockOutcome::TransportFailure => Err(Error::ScsiError {
opcode: cdb[0],
status: 0xFF,
sense_key: 0,
}),
MockOutcome::ScsiFailure { status, sense_key } => Err(Error::ScsiError {
opcode: cdb[0],
status,
sense_key,
}),
}
}
}
#[test]
fn test_healthy_inquiry_returns_ok_with_full_transfer() {
let mut payload = vec![0u8; 96];
payload[8..16].copy_from_slice(b"TEST-VND");
payload[16..32].copy_from_slice(b"TEST-MODEL ");
payload[32..36].copy_from_slice(b"1.00");
let mut transport = MockTransport::new(vec![MockOutcome::Ok {
data: payload,
resid: 0,
}]);
let r = libfreemkv::scsi::inquiry(&mut transport).expect("inquiry should succeed");
assert_eq!(r.vendor_id, "TEST-VND");
assert_eq!(r.model, "TEST-MODEL");
assert_eq!(r.firmware, "1.00");
}
#[test]
fn test_transport_failure_surfaces_as_status_0xff_sense_key_0() {
let mut transport = MockTransport::new(vec![MockOutcome::TransportFailure]);
let err = libfreemkv::scsi::inquiry(&mut transport).unwrap_err();
match err {
Error::ScsiError {
status, sense_key, ..
} => {
assert_eq!(status, 0xFF, "transport failure must surface as 0xFF");
assert_eq!(sense_key, 0, "transport failure has no sense key");
}
other => panic!("expected ScsiError, got {other:?}"),
}
}
#[test]
fn test_scsi_failure_descriptor_format_illegal_request() {
let mut transport = MockTransport::new(vec![MockOutcome::ScsiFailure {
status: 0x02,
sense_key: 5,
}]);
let err = libfreemkv::scsi::inquiry(&mut transport).unwrap_err();
match err {
Error::ScsiError {
opcode,
status,
sense_key,
} => {
assert_eq!(opcode, libfreemkv::scsi::SCSI_INQUIRY);
assert_eq!(status, 0x02);
assert_eq!(sense_key, 5);
}
other => panic!("expected ScsiError, got {other:?}"),
}
}
#[test]
fn test_scsi_failure_not_ready_key_2_propagates_intact() {
let mut transport = MockTransport::new(vec![MockOutcome::ScsiFailure {
status: 0x02,
sense_key: 2,
}]);
let err = libfreemkv::scsi::inquiry(&mut transport).unwrap_err();
match err {
Error::ScsiError {
status, sense_key, ..
} => {
assert_eq!(status, 0x02);
assert_eq!(sense_key, 2);
}
other => panic!("expected ScsiError, got {other:?}"),
}
}
#[test]
fn test_scsi_failure_empty_sense_returns_key_0() {
let mut transport = MockTransport::new(vec![MockOutcome::ScsiFailure {
status: 0x02,
sense_key: 0,
}]);
let err = libfreemkv::scsi::inquiry(&mut transport).unwrap_err();
match err {
Error::ScsiError {
status, sense_key, ..
} => {
assert_eq!(status, 0x02);
assert_eq!(sense_key, 0);
}
other => panic!("expected ScsiError, got {other:?}"),
}
}
#[test]
fn test_healthy_short_transfer_reports_partial_bytes() {
let payload = vec![0u8; 96];
let mut transport = MockTransport::new(vec![MockOutcome::Ok {
data: payload,
resid: 16,
}]);
let cdb = [libfreemkv::scsi::SCSI_INQUIRY, 0, 0, 0, 0x60, 0];
let mut buf = [0u8; 96];
let r = transport
.execute(&cdb, DataDirection::FromDevice, &mut buf, 1_000)
.expect("ok");
assert_eq!(r.status, 0);
assert_eq!(
r.bytes_transferred, 80,
"bytes_transferred must equal data.len() - resid"
);
}
#[test]
fn test_scsi_error_display_format_is_codes_only() {
let err = Error::ScsiError {
opcode: 0x12,
status: 0x02,
sense_key: 5,
};
let s = err.to_string();
assert!(s.starts_with("E4000:"), "ScsiError must lead with E4000: {s}");
assert!(
s.contains("0x12") && s.contains("0x02") && s.contains("0x05"),
"ScsiError must show opcode/status/sense_key in hex: {s}"
);
for word in s.split(|c: char| !c.is_ascii_alphabetic()) {
assert!(
word.len() <= 4,
"ScsiError display contains suspicious word `{word}`: {s}"
);
}
}