Skip to content

Commit 18796a8

Browse files
authored
test(ssh-console): table-ify cert and escape-sequence tests; cover BmcVendor (#2736)
1 parent 9d13b8e commit 18796a8

4 files changed

Lines changed: 282 additions & 292 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ssh-console/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ size = { features = ["serde"], workspace = true }
7777
[dev-dependencies]
7878
bmc-mock = { path = "../bmc-mock" }
7979
carbide-machine-a-tron = { path = "../machine-a-tron" }
80+
carbide-test-support = { path = "../test-support" }
8081
temp-dir = { workspace = true }
8182
futures = { workspace = true }
8283
carbide-api-test-helper = { path = "../api-test-helper" }

crates/ssh-console/src/bmc/vendor.rs

Lines changed: 188 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -317,146 +317,197 @@ impl EscapeSequence {
317317
}
318318
}
319319

320-
#[test]
321-
fn test_filter_escape_sequence() {
322-
// Pass-through: no escapes
323-
{
324-
let result =
325-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"hello world", false);
326-
assert_eq!(result, (Cow::Borrowed(b"hello world".as_slice()), false));
327-
// Make sure we don't allocate
328-
assert!(matches!(result.0, Cow::Borrowed(_)));
320+
#[cfg(test)]
321+
mod tests {
322+
use carbide_test_support::Outcome::*;
323+
use carbide_test_support::{scenarios, value_scenarios};
324+
325+
use super::*;
326+
327+
/// One row of the escape-sequence filtering table: which vendor escape to
328+
/// apply, the input bytes, and whether the previous slice ended mid-escape.
329+
struct FilterCase {
330+
escape: EscapeSequence,
331+
input: &'static [u8],
332+
prev_pending: bool,
329333
}
330334

331-
// Only a trailing pending escape byte
332-
assert_eq!(
333-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"hello world\x1b", false),
334-
(Cow::Borrowed(b"hello world".as_slice()), true)
335-
);
336-
337-
assert_eq!(
338-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"\x28", true),
339-
(Cow::Borrowed(b"".as_slice()), false)
340-
);
341-
342-
assert!(
343-
!EscapeSequence::Pair((0x1b, &[0x28]))
344-
.filter_escape_sequences(&[0x1b, 0x1b, 0x28, 0x28], false)
345-
.0
346-
.windows(2)
347-
.any(|w| w[0] == 0x1b && w[1] == 0x28)
348-
);
349-
350-
assert!(
351-
!EscapeSequence::Pair((0x1b, &[0x28]))
352-
.filter_escape_sequences(&[0x1b, 0x28, 0x28], true)
353-
.0
354-
.windows(2)
355-
.any(|w| w[0] == 0x1b && w[1] == 0x28)
356-
);
357-
358-
assert_eq!(
359-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"\x1b", false),
360-
(Cow::Borrowed(b"".as_slice()), true)
361-
);
362-
363-
assert_eq!(
364-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"hello world\x1b!", false),
365-
(Cow::Borrowed(b"hello world\x1b!".as_slice()), false)
366-
);
367-
368-
assert_eq!(
369-
EscapeSequence::Pair((0x1b, &[0x28]))
370-
.filter_escape_sequences(b"hello \x1b\x28 world", false),
371-
(Cow::Borrowed(b"hello world".as_slice()), false)
372-
);
373-
374-
assert_eq!(
375-
EscapeSequence::Pair((0x1b, &[0x28]))
376-
.filter_escape_sequences(b"hello world\x1b\x28", false),
377-
(Cow::Borrowed(b"hello world".as_slice()), false)
378-
);
379-
380-
assert_eq!(
381-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"Z", true),
382-
(Cow::Borrowed(b"\x1bZ".as_slice()), false)
383-
);
384-
385-
assert_eq!(
386-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"hello world", true),
387-
(Cow::Borrowed(b"\x1bhello world".as_slice()), false)
388-
);
389-
390-
assert_eq!(
391-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"\x28hello world", true),
392-
(Cow::Borrowed(b"hello world".as_slice()), false)
393-
);
394-
395-
assert_eq!(
396-
EscapeSequence::Pair((0x1b, &[0x28])).filter_escape_sequences(b"\x28hello world\x1b", true),
397-
(Cow::Borrowed(b"hello world".as_slice()), true)
398-
);
335+
/// The Lenovo/HPE two-byte escape (`ESC (`), used by most filtering rows.
336+
const ESC_PAREN: EscapeSequence = EscapeSequence::Pair((0x1b, &[0x28]));
337+
338+
#[test]
339+
fn filter_escape_sequences_removes_escapes_and_tracks_pending() {
340+
// Each row runs `filter_escape_sequences`, projecting the borrowed/owned
341+
// `Cow` to an owned `Vec<u8>` so the asserted output and the pending flag
342+
// both stay visible per case.
343+
scenarios!(
344+
run = |FilterCase { escape, input, prev_pending }| {
345+
let (output, pending) = escape.filter_escape_sequences(input, prev_pending);
346+
Ok::<_, ()>((output.into_owned(), pending))
347+
};
348+
349+
"ESC ( pass-through and pending lead" {
350+
FilterCase { escape: ESC_PAREN, input: b"hello world", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
351+
FilterCase { escape: ESC_PAREN, input: b"hello world\x1b", prev_pending: false } => Yields((b"hello world".to_vec(), true)),
352+
FilterCase { escape: ESC_PAREN, input: b"\x1b", prev_pending: false } => Yields((b"".to_vec(), true)),
353+
FilterCase { escape: ESC_PAREN, input: b"hello world\x1b!", prev_pending: false } => Yields((b"hello world\x1b!".to_vec(), false)),
354+
}
399355

400-
{
401-
let result = EscapeSequence::Single(0x1b).filter_escape_sequences(b"hello world", false);
402-
assert_eq!(result, (Cow::Borrowed(b"hello world".as_slice()), false));
403-
// Make sure we don't allocate if there's no sequence
404-
assert!(matches!(result.0, Cow::Borrowed(_)))
356+
"ESC ( escape removed mid-stream" {
357+
FilterCase { escape: ESC_PAREN, input: b"hello \x1b\x28 world", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
358+
FilterCase { escape: ESC_PAREN, input: b"hello world\x1b\x28", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
359+
}
360+
361+
"ESC ( with a pending lead from the previous slice" {
362+
FilterCase { escape: ESC_PAREN, input: b"\x28", prev_pending: true } => Yields((b"".to_vec(), false)),
363+
FilterCase { escape: ESC_PAREN, input: b"Z", prev_pending: true } => Yields((b"\x1bZ".to_vec(), false)),
364+
FilterCase { escape: ESC_PAREN, input: b"hello world", prev_pending: true } => Yields((b"\x1bhello world".to_vec(), false)),
365+
FilterCase { escape: ESC_PAREN, input: b"\x28hello world", prev_pending: true } => Yields((b"hello world".to_vec(), false)),
366+
FilterCase { escape: ESC_PAREN, input: b"\x28hello world\x1b", prev_pending: true } => Yields((b"hello world".to_vec(), true)),
367+
}
368+
369+
"single-byte (Dell ctrl+\\) escape removed" {
370+
FilterCase { escape: EscapeSequence::Single(0x1b), input: b"hello world", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
371+
FilterCase { escape: EscapeSequence::Single(0x1c), input: b"hello \x1c world", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
372+
FilterCase { escape: EscapeSequence::Single(0x1c), input: b"hello world\x1c", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
373+
FilterCase { escape: EscapeSequence::Single(0x1c), input: b"\x1chello world", prev_pending: false } => Yields((b"hello world".to_vec(), false)),
374+
FilterCase { escape: EscapeSequence::Single(0x1c), input: b"\x1c", prev_pending: false } => Yields((b"".to_vec(), false)),
375+
}
376+
377+
"ipmitool multi-trail escape" {
378+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: b"~~", prev_pending: false } => Yields((b"~".to_vec(), true)),
379+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: b"~~~", prev_pending: false } => Yields((b"~~".to_vec(), true)),
380+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: b"~~.", prev_pending: false } => Yields((b"~".to_vec(), false)),
381+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: b"~.", prev_pending: false } => Yields((b"".to_vec(), false)),
382+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: b"~B", prev_pending: false } => Yields((b"".to_vec(), false)),
383+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: &[b'~', 0x1a], prev_pending: false } => Yields((b"".to_vec(), false)),
384+
FilterCase { escape: IPMITOOL_ESCAPE_SEQUENCE, input: &[b'~', 0x18], prev_pending: false } => Yields((b"".to_vec(), false)),
385+
}
386+
);
387+
388+
// A clean stream is returned borrowed, with no allocation. This asserts a
389+
// structural property (the `Cow` variant) that the value table above can't
390+
// express, so it stays a hand-written check.
391+
assert!(matches!(
392+
ESC_PAREN.filter_escape_sequences(b"hello world", false).0,
393+
Cow::Borrowed(_)
394+
));
395+
assert!(matches!(
396+
EscapeSequence::Single(0x1b)
397+
.filter_escape_sequences(b"hello world", false)
398+
.0,
399+
Cow::Borrowed(_)
400+
));
401+
402+
// Adjacent escapes must not leave a reconstructable `ESC (` pair in the
403+
// output; assert that absence directly rather than the exact bytes.
404+
assert!(
405+
!ESC_PAREN
406+
.filter_escape_sequences(&[0x1b, 0x1b, 0x28, 0x28], false)
407+
.0
408+
.windows(2)
409+
.any(|w| w[0] == 0x1b && w[1] == 0x28)
410+
);
411+
assert!(
412+
!ESC_PAREN
413+
.filter_escape_sequences(&[0x1b, 0x28, 0x28], true)
414+
.0
415+
.windows(2)
416+
.any(|w| w[0] == 0x1b && w[1] == 0x28)
417+
);
418+
}
419+
420+
/// Wraps a [`BmcVendor`] in a struct field so its custom serde is exercised
421+
/// through a real (de)serializer; the field is a plain TOML string.
422+
#[derive(Debug, PartialEq, Serialize, Deserialize)]
423+
struct Wrap {
424+
vendor: BmcVendor,
405425
}
406426

407-
assert_eq!(
408-
EscapeSequence::Single(0x1c).filter_escape_sequences(b"hello \x1c world", false),
409-
(Cow::Borrowed(b"hello world".as_slice()), false)
410-
);
411-
412-
assert_eq!(
413-
EscapeSequence::Single(0x1c).filter_escape_sequences(b"hello world\x1c", false),
414-
(Cow::Borrowed(b"hello world".as_slice()), false)
415-
);
416-
417-
assert_eq!(
418-
EscapeSequence::Single(0x1c).filter_escape_sequences(b"\x1chello world", false),
419-
(Cow::Borrowed(b"hello world".as_slice()), false)
420-
);
421-
422-
assert_eq!(
423-
EscapeSequence::Single(0x1c).filter_escape_sequences(b"\x1c", false),
424-
(Cow::Borrowed(b"".as_slice()), false)
425-
);
426-
427-
let ipmitool_escape_sequence = IPMITOOL_ESCAPE_SEQUENCE;
428-
assert_eq!(
429-
ipmitool_escape_sequence.filter_escape_sequences(b"~~", false),
430-
(Cow::Borrowed(b"~".as_slice()), true)
431-
);
432-
433-
assert_eq!(
434-
ipmitool_escape_sequence.filter_escape_sequences(b"~~~", false),
435-
(Cow::Borrowed(b"~~".as_slice()), true)
436-
);
437-
438-
assert_eq!(
439-
ipmitool_escape_sequence.filter_escape_sequences(b"~~.", false),
440-
(Cow::Borrowed(b"~".as_slice()), false)
441-
);
442-
443-
assert_eq!(
444-
ipmitool_escape_sequence.filter_escape_sequences(b"~.", false),
445-
(Cow::Borrowed(b"".as_slice()), false)
446-
);
447-
448-
assert_eq!(
449-
ipmitool_escape_sequence.filter_escape_sequences(b"~B", false),
450-
(Cow::Borrowed(b"".as_slice()), false)
451-
);
452-
453-
assert_eq!(
454-
ipmitool_escape_sequence.filter_escape_sequences(&[b'~', 0x1a], false),
455-
(Cow::Borrowed(b"".as_slice()), false)
456-
);
457-
458-
assert_eq!(
459-
ipmitool_escape_sequence.filter_escape_sequences(&[b'~', 0x18], false),
460-
(Cow::Borrowed(b"".as_slice()), false)
461-
);
427+
const ALL_VENDORS: [(BmcVendor, &str); 7] = [
428+
(BmcVendor::Ssh(SshBmcVendor::Dell), "dell"),
429+
(BmcVendor::Ssh(SshBmcVendor::Lenovo), "lenovo"),
430+
(BmcVendor::Ssh(SshBmcVendor::LenovoAmi), "lenovo_ami"),
431+
(BmcVendor::Ssh(SshBmcVendor::Hpe), "hpe"),
432+
(BmcVendor::Ssh(SshBmcVendor::Dpu), "dpu"),
433+
(BmcVendor::Ipmi(IpmiBmcVendor::Supermicro), "supermicro"),
434+
(
435+
BmcVendor::Ipmi(IpmiBmcVendor::NvidiaViking),
436+
"nvidia_viking",
437+
),
438+
];
439+
440+
#[test]
441+
fn bmc_vendor_config_string_names_each_variant() {
442+
// Driven from `ALL_VENDORS` so a new variant is covered by adding one row there.
443+
for (vendor, config_string) in ALL_VENDORS {
444+
assert_eq!(vendor.config_string(), config_string, "{vendor:?}");
445+
}
446+
}
447+
448+
#[test]
449+
fn bmc_vendor_from_config_string_parses_each_variant() {
450+
// Driven from `ALL_VENDORS` so a new variant is covered by adding one row there.
451+
for (vendor, config_string) in ALL_VENDORS {
452+
assert_eq!(
453+
BmcVendor::from_config_string(config_string),
454+
Some(vendor),
455+
"{config_string:?}",
456+
);
457+
}
458+
459+
value_scenarios!(
460+
run = |s: &str| BmcVendor::from_config_string(s);
461+
462+
"an unknown string has no vendor" {
463+
"" => None,
464+
"bogus" => None,
465+
}
466+
);
467+
}
468+
469+
#[test]
470+
fn bmc_vendor_config_string_round_trips() {
471+
// config_string -> from_config_string returns the same vendor for every variant.
472+
for (vendor, _) in ALL_VENDORS {
473+
assert_eq!(
474+
BmcVendor::from_config_string(vendor.config_string()),
475+
Some(vendor),
476+
"{vendor:?}",
477+
);
478+
}
479+
}
480+
481+
#[test]
482+
fn bmc_vendor_serde_round_trips_through_toml() {
483+
// The custom Serialize/Deserialize go through a real TOML (de)serializer.
484+
for (vendor, config_string) in ALL_VENDORS {
485+
let label = format!("{vendor:?}");
486+
let wrap = Wrap { vendor };
487+
let serialized = toml::to_string(&wrap).expect("serialize");
488+
assert_eq!(
489+
serialized,
490+
format!("vendor = \"{config_string}\"\n"),
491+
"{label}"
492+
);
493+
let deserialized: Wrap = toml::from_str(&serialized).expect("deserialize");
494+
assert_eq!(deserialized, wrap, "{label}");
495+
}
496+
}
497+
498+
#[test]
499+
fn bmc_vendor_deserialize_rejects_an_unknown_string() {
500+
scenarios!(
501+
run = |s: &str| toml::from_str::<Wrap>(&format!("vendor = \"{s}\"")).map_err(|e| e.to_string());
502+
503+
"valid config strings deserialize" {
504+
"dell" => Yields(Wrap { vendor: BmcVendor::Ssh(SshBmcVendor::Dell) }),
505+
"nvidia_viking" => Yields(Wrap { vendor: BmcVendor::Ipmi(IpmiBmcVendor::NvidiaViking) }),
506+
}
507+
508+
"an unknown string is rejected" {
509+
"bogus" => Fails,
510+
}
511+
);
512+
}
462513
}

0 commit comments

Comments
 (0)