@@ -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"\x1b Z" . as_slice( ) ) , false )
383- ) ;
384-
385- assert_eq ! (
386- EscapeSequence :: Pair ( ( 0x1b , & [ 0x28 ] ) ) . filter_escape_sequences( b"hello world" , true ) ,
387- ( Cow :: Borrowed ( b"\x1b hello world" . as_slice( ) ) , false )
388- ) ;
389-
390- assert_eq ! (
391- EscapeSequence :: Pair ( ( 0x1b , & [ 0x28 ] ) ) . filter_escape_sequences( b"\x28 hello 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"\x28 hello 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"\x1b Z" . to_vec( ) , false ) ) ,
364+ FilterCase { escape: ESC_PAREN , input: b"hello world" , prev_pending: true } => Yields ( ( b"\x1b hello world" . to_vec( ) , false ) ) ,
365+ FilterCase { escape: ESC_PAREN , input: b"\x28 hello world" , prev_pending: true } => Yields ( ( b"hello world" . to_vec( ) , false ) ) ,
366+ FilterCase { escape: ESC_PAREN , input: b"\x28 hello 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"\x1c hello 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"\x1c hello 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