Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
8.5 KiB
Rust
235 lines
8.5 KiB
Rust
use rstest::rstest;
|
|
|
|
use super::*;
|
|
|
|
#[rstest]
|
|
#[case::noo_param("", None, Type::Format, None)]
|
|
#[case::one_param_type("=u8", None, Type::U8, None)]
|
|
#[case::one_param_hint(":a", None, Type::Format, Some(DisplayHint::Ascii))]
|
|
#[case::one_param_index("1", Some(1), Type::Format, None)]
|
|
#[case::two_param_type_hint("=u8:x", None, Type::U8, Some(DisplayHint::Hexadecimal {alternate: false, uppercase: false, zero_pad: 0}))]
|
|
#[case::two_param_index_type("0=u8", Some(0), Type::U8, None)]
|
|
#[case::two_param_index_hint("0:a", Some(0), Type::Format, Some(DisplayHint::Ascii))]
|
|
#[case::two_param_type_hint("=[u8]:#04x", None, Type::U8Slice, Some(DisplayHint::Hexadecimal {alternate: true, uppercase: false, zero_pad: 4}))]
|
|
#[case::all_param("1=u8:b", Some(1), Type::U8, Some(DisplayHint::Binary { alternate: false, zero_pad: 0}))]
|
|
fn all_parse_param_cases(
|
|
#[case] input: &str,
|
|
#[case] index: Option<usize>,
|
|
#[case] ty: Type,
|
|
#[case] hint: Option<DisplayHint>,
|
|
) {
|
|
assert_eq!(
|
|
parse_param(input, ParserMode::Strict),
|
|
Ok(Param { index, ty, hint })
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case(":a", DisplayHint::Ascii)]
|
|
#[case(":b", DisplayHint::Binary { alternate: false, zero_pad: 0 })]
|
|
#[case(":#b", DisplayHint::Binary { alternate: true, zero_pad: 0 })]
|
|
#[case(":o", DisplayHint::Octal { alternate: false, zero_pad: 0 })]
|
|
#[case(":#o", DisplayHint::Octal { alternate: true, zero_pad: 0 })]
|
|
#[case(":x", DisplayHint::Hexadecimal { alternate: false, uppercase: false, zero_pad: 0 })]
|
|
#[case(":02x", DisplayHint::Hexadecimal { alternate: false, uppercase: false, zero_pad: 2 })]
|
|
#[case(":#x", DisplayHint::Hexadecimal { alternate: true, uppercase: false, zero_pad: 0 })]
|
|
#[case(":#04x", DisplayHint::Hexadecimal { alternate: true, uppercase: false, zero_pad: 4 })]
|
|
#[case(":X", DisplayHint::Hexadecimal { alternate: false, uppercase: true, zero_pad: 0 })]
|
|
#[case(":#X", DisplayHint::Hexadecimal { alternate: true, uppercase: true, zero_pad: 0 })]
|
|
#[case(":ms", DisplayHint::Seconds(TimePrecision::Millis))]
|
|
#[case(":us", DisplayHint::Seconds(TimePrecision::Micros))]
|
|
#[case(":ts", DisplayHint::Time(TimePrecision::Seconds))]
|
|
#[case(":tms", DisplayHint::Time(TimePrecision::Millis))]
|
|
#[case(":tus", DisplayHint::Time(TimePrecision::Micros))]
|
|
#[case(":iso8601ms", DisplayHint::ISO8601(TimePrecision::Millis))]
|
|
#[case(":iso8601s", DisplayHint::ISO8601(TimePrecision::Seconds))]
|
|
#[case(":?", DisplayHint::Debug)]
|
|
#[case(":02", DisplayHint::NoHint { zero_pad: 2 })]
|
|
fn all_display_hints(#[case] input: &str, #[case] hint: DisplayHint) {
|
|
assert_eq!(
|
|
parse_param(input, ParserMode::Strict),
|
|
Ok(Param {
|
|
index: None,
|
|
ty: Type::Format,
|
|
hint: Some(hint),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
// separate test, because of `ParserMode::ForwardsCompatible`
|
|
fn display_hint_unknown() {
|
|
assert_eq!(
|
|
parse_param(":unknown", ParserMode::ForwardsCompatible),
|
|
Ok(Param {
|
|
index: None,
|
|
ty: Type::Format,
|
|
hint: Some(DisplayHint::Unknown("unknown".to_string())),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("=i8", Type::I8)]
|
|
#[case("=i16", Type::I16)]
|
|
#[case("=i32", Type::I32)]
|
|
#[case("=i64", Type::I64)]
|
|
#[case("=i128", Type::I128)]
|
|
#[case("=isize", Type::Isize)]
|
|
#[case("=u8", Type::U8)]
|
|
#[case("=u16", Type::U16)]
|
|
#[case("=u32", Type::U32)]
|
|
#[case("=u64", Type::U64)]
|
|
#[case("=u128", Type::U128)]
|
|
#[case("=usize", Type::Usize)]
|
|
#[case("=f32", Type::F32)]
|
|
#[case("=f64", Type::F64)]
|
|
#[case("=bool", Type::Bool)]
|
|
#[case("=?", Type::Format)]
|
|
#[case("=str", Type::Str)]
|
|
#[case("=[u8]", Type::U8Slice)]
|
|
fn all_types(#[case] input: &str, #[case] ty: Type) {
|
|
assert_eq!(
|
|
parse_param(input, ParserMode::Strict),
|
|
Ok(Param {
|
|
index: None,
|
|
ty,
|
|
hint: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::implicit("{=u8}{=u16}", [(0, Type::U8), (1, Type::U16)])]
|
|
#[case::single_parameter_formatted_twice("{=u8}{0=u8}", [(0, Type::U8), (0, Type::U8)])]
|
|
#[case::explicit_index("{=u8}{1=u16}", [(0, Type::U8), (1, Type::U16)])]
|
|
#[case::reversed_order("{1=u8}{0=u16}", [(1, Type::U8), (0, Type::U16)])]
|
|
fn index(#[case] input: &str, #[case] params: [(usize, Type); 2]) {
|
|
assert_eq!(
|
|
parse(input, ParserMode::Strict),
|
|
Ok(vec![
|
|
Fragment::Parameter(Parameter {
|
|
index: params[0].0,
|
|
ty: params[0].1.clone(),
|
|
hint: None,
|
|
}),
|
|
Fragment::Parameter(Parameter {
|
|
index: params[1].0,
|
|
ty: params[1].1.clone(),
|
|
hint: None,
|
|
}),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("{=0..4}", 0..4)]
|
|
#[case::just_inside_128bit_range_1("{=0..128}", 0..128)]
|
|
#[case::just_inside_128bit_range_2("{=127..128}", 127..128)]
|
|
fn range(#[case] input: &str, #[case] bit_field: Range<u8>) {
|
|
assert_eq!(
|
|
parse(input, ParserMode::Strict),
|
|
Ok(vec![Fragment::Parameter(Parameter {
|
|
index: 0,
|
|
ty: Type::BitField(bit_field),
|
|
hint: None,
|
|
})])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_ranges() {
|
|
assert_eq!(
|
|
parse("{0=30..31}{1=0..4}{1=2..6}", ParserMode::Strict),
|
|
Ok(vec![
|
|
Fragment::Parameter(Parameter {
|
|
index: 0,
|
|
ty: Type::BitField(30..31),
|
|
hint: None,
|
|
}),
|
|
Fragment::Parameter(Parameter {
|
|
index: 1,
|
|
ty: Type::BitField(0..4),
|
|
hint: None,
|
|
}),
|
|
Fragment::Parameter(Parameter {
|
|
index: 1,
|
|
ty: Type::BitField(2..6),
|
|
hint: None,
|
|
}),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("{=[u8; 0]}", 0)]
|
|
#[case::space_is_optional("{=[u8;42]}", 42)]
|
|
#[case::multiple_spaces_are_ok("{=[u8; 257]}", 257)]
|
|
fn arrays(#[case] input: &str, #[case] length: usize) {
|
|
assert_eq!(
|
|
parse(input, ParserMode::Strict),
|
|
Ok(vec![Fragment::Parameter(Parameter {
|
|
index: 0,
|
|
ty: Type::U8Array(length),
|
|
hint: None,
|
|
})])
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::no_tabs("{=[u8; \t 3]}")]
|
|
#[case::no_linebreaks("{=[u8; \n 3]}")]
|
|
#[case::too_large("{=[u8; 9999999999999999999999999]}")]
|
|
fn arrays_err(#[case] input: &str) {
|
|
assert!(parse(input, ParserMode::Strict).is_err());
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("{=dunno}", Error::InvalidTypeSpecifier("dunno".to_string()))]
|
|
#[case("{dunno}", Error::UnexpectedContentInFormatString("dunno".to_string()))]
|
|
#[case("{=u8;x}", Error::InvalidTypeSpecifier("u8;x".to_string()))]
|
|
#[case("{dunno=u8:x}", Error::UnexpectedContentInFormatString("dunno=u8:x".to_string()))]
|
|
#[case("{0dunno}", Error::UnexpectedContentInFormatString("dunno".to_string()))]
|
|
#[case("{:}", Error::MalformedFormatString)]
|
|
#[case::stray_braces_1("}string", Error::UnmatchedCloseBracket)]
|
|
#[case::stray_braces_2("{string", Error::UnmatchedOpenBracket)]
|
|
#[case::stray_braces_3("}", Error::UnmatchedCloseBracket)]
|
|
#[case::stray_braces_4("{", Error::UnmatchedOpenBracket)]
|
|
#[case::range_empty("{=0..0}", Error::InvalidTypeSpecifier("0..0".to_string()))]
|
|
#[case::range_start_gt_end("{=1..0}", Error::InvalidTypeSpecifier("1..0".to_string()))]
|
|
#[case::range_out_of_128bit_1("{=0..129}", Error::InvalidTypeSpecifier("0..129".to_string()))]
|
|
#[case::range_out_of_128bit_2("{=128..128}", Error::InvalidTypeSpecifier("128..128".to_string()))]
|
|
#[case::range_missing_parts_1("{=0..4", Error::UnmatchedOpenBracket)]
|
|
#[case::range_missing_parts_2("{=0..}", Error::InvalidTypeSpecifier("0..".to_string()))]
|
|
#[case::range_missing_parts_3("{=..4}", Error::InvalidTypeSpecifier("..4".to_string()))]
|
|
#[case::range_missing_parts_4("{=0.4}", Error::InvalidTypeSpecifier("0.4".to_string()))]
|
|
#[case::range_missing_parts_5("{=0...4}", Error::InvalidTypeSpecifier("0...4".to_string()))]
|
|
#[case::index_with_different_types(
|
|
"{0=u8}{0=u16}",
|
|
Error::ConflictingTypes(0, Type::U8, Type::U16)
|
|
)]
|
|
#[case::index_with_different_types_bool_is_autoassigned_index_0(
|
|
"Hello {1=u16} {0=u8} {=bool}",
|
|
Error::ConflictingTypes(0, Type::U8, Type::Bool)
|
|
)]
|
|
#[case::index_0_is_omitted("{1=u8}", Error::UnusedArgument(0))]
|
|
#[case::index_1_is_missing("{2=u8}{=u16}", Error::UnusedArgument(1))]
|
|
#[case::index_0_is_missing("{2=u8}{1=u16}", Error::UnusedArgument(0))]
|
|
fn error_msg(#[case] input: &str, #[case] err: Error) {
|
|
assert_eq!(parse(input, ParserMode::Strict), Err(err));
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("}}", "}")]
|
|
#[case("{{", "{")]
|
|
#[case("literal{{literal", "literal{literal")]
|
|
#[case("literal}}literal", "literal}literal")]
|
|
#[case("{{}}", "{}")]
|
|
#[case("}}{{", "}{")]
|
|
fn escaped_braces(#[case] input: &str, #[case] literal: &str) {
|
|
assert_eq!(
|
|
parse(input, ParserMode::Strict),
|
|
Ok(vec![Fragment::Literal(literal.into())])
|
|
);
|
|
}
|