diff --git a/crates/api-model/src/allocation_type.rs b/crates/api-model/src/allocation_type.rs index dd6e3ef730..309c2a0781 100644 --- a/crates/api-model/src/allocation_type.rs +++ b/crates/api-model/src/allocation_type.rs @@ -270,9 +270,7 @@ mod tests { ); } - // Derived `PartialEq`/`Eq` over both `AllocationType` and `AssignStaticResult`: - // a variant equals only itself. `AssignStaticResult` has no other pure logic, so - // equality across its full variant set is its coverage. + // Derived `PartialEq`/`Eq` over `AllocationType`: a variant equals only itself. #[test] fn variants_compare_by_identity() { value_scenarios!( @@ -301,47 +299,5 @@ mod tests { (AllocationType::Static, AllocationType::Slaac) => false, } ); - - value_scenarios!( - run = |(left, right)| left == right; - "assigned == assigned" { - (AssignStaticResult::Assigned, AssignStaticResult::Assigned) => true, - } - - "replaced static == replaced static" { - ( - AssignStaticResult::ReplacedStatic, - AssignStaticResult::ReplacedStatic, - ) => true, - } - - "replaced dhcp == replaced dhcp" { - ( - AssignStaticResult::ReplacedDhcp, - AssignStaticResult::ReplacedDhcp, - ) => true, - } - - "assigned != replaced static" { - ( - AssignStaticResult::Assigned, - AssignStaticResult::ReplacedStatic, - ) => false, - } - - "replaced static != replaced dhcp" { - ( - AssignStaticResult::ReplacedStatic, - AssignStaticResult::ReplacedDhcp, - ) => false, - } - - "assigned != replaced dhcp" { - ( - AssignStaticResult::Assigned, - AssignStaticResult::ReplacedDhcp, - ) => false, - } - ); } } diff --git a/crates/api-model/src/controller_outcome.rs b/crates/api-model/src/controller_outcome.rs index 3828720f1f..7f25b438f6 100644 --- a/crates/api-model/src/controller_outcome.rs +++ b/crates/api-model/src/controller_outcome.rs @@ -279,45 +279,6 @@ mod tests { ); } - // Display for both types delegates to Debug. The rendered String is the - // contract; comparing against the Debug rendering keeps the rows honest - // without hard-coding the exact derived layout. - #[test] - fn test_display_matches_debug() { - value_scenarios!( - run = |rendered| rendered; - "source reference display equals debug" { - format!("{}", source_ref()) => format!("{:?}", source_ref()), - } - - "transition outcome display equals debug" { - format!( - "{}", - PersistentStateHandlerOutcome::Transition { source_ref: None } - ) => format!( - "{:?}", - PersistentStateHandlerOutcome::Transition { source_ref: None } - ), - } - - "wait outcome display equals debug" { - format!( - "{}", - PersistentStateHandlerOutcome::Wait { - reason: "r".to_string(), - source_ref: Some(source_ref()), - } - ) => format!( - "{:?}", - PersistentStateHandlerOutcome::Wait { - reason: "r".to_string(), - source_ref: Some(source_ref()), - } - ), - } - ); - } - // Display for the source reference contains both the file and the line. #[test] fn test_source_reference_display_tokens() { diff --git a/crates/api-model/src/machine/network.rs b/crates/api-model/src/machine/network.rs index 9b71859a8a..db3dad30b3 100644 --- a/crates/api-model/src/machine/network.rs +++ b/crates/api-model/src/machine/network.rs @@ -405,50 +405,6 @@ mod tests { .check(|qs| qs); } - // Verify that IpAddr::to_string() produces the expected format for both - // address families, since several call sites throughout the codebase - // use .to_string() on the loopback_ip value. Folded from a pair of - // hand-written asserts into a table covering both families plus the - // boundary/canonicalization cases (zero, broadcast, all-ones, - // loopback, embedded-IPv4) where formatting can surprise. - #[test] - fn test_ip_addr_to_string_format() { - value_scenarios!( - run = |ip| ip.to_string(); - "ipv4" { - IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)) => "10.0.0.1".to_string(), - } - - "ipv4 unspecified" { - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) => "0.0.0.0".to_string(), - } - - "ipv4 broadcast" { - IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)) => "255.255.255.255".to_string(), - } - - "ipv4 loopback" { - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) => "127.0.0.1".to_string(), - } - - "ipv6 compressed" { - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)) => "2001:db8::1".to_string(), - } - - "ipv6 unspecified collapses to ::" { - IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)) => "::".to_string(), - } - - "ipv6 loopback collapses to ::1" { - IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)) => "::1".to_string(), - } - - "ipv6 full (no run to compress)" { - IpAddr::V6(Ipv6Addr::new(0xfd00, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x42)) => "fd00:1:2:3:4:5:6:42".to_string(), - } - ); - } - // ManagedHostQuarantineState::reason_str() returns the reason or an empty // string when None. ManagedHostQuarantineMode has a single variant today; // its as_str()/mode_str() must render it exactly so the persisted form is @@ -613,68 +569,6 @@ mod tests { ); } - // Parse pool strings as IpAddr (resource pools store values as strings and - // parse them via IpAddr::from_str). Yielding the exact IpAddr value also - // covers the original is_ipv4()/is_ipv6() family assertions. AddrParseError - // is not PartialEq, so failing rows would use `Fails`; both rows parse. - #[test] - fn test_ip_addr_parse_from_pool_strings() { - scenarios!( - run = |s| s.parse::().map_err(drop); - "ipv4 string" { - "10.0.0.1" => Yields(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), - } - - "ipv6 string" { - "2001:db8::1" => Yields(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))), - } - - "ipv4 unspecified" { - "0.0.0.0" => Yields(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - } - - "ipv4 broadcast" { - "255.255.255.255" => Yields(IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255))), - } - - "ipv6 unspecified" { - "::" => Yields(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0))), - } - - "ipv6 loopback" { - "::1" => Yields(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))), - } - - "empty string is rejected" { - "" => Fails, - } - - "non-address text is rejected" { - "not-an-ip" => Fails, - } - - "ipv4 octet out of range is rejected" { - "256.0.0.1" => Fails, - } - - "ipv4 with too few octets is rejected" { - "10.0.0" => Fails, - } - - "ipv4 with trailing whitespace is rejected" { - "10.0.0.1 " => Fails, - } - - "ipv6 with double :: is rejected" { - "2001::db8::1" => Fails, - } - - "cidr suffix is not an address" { - "10.0.0.0/24" => Fails, - } - ); - } - // Deserialize raw JSON whose shape is malformed or whose IP strings are // invalid; serde_json::Error is not PartialEq, so rejected rows use `Fails`. // Also covers a populated quarantine_state, which the earlier deserialize diff --git a/crates/api-model/src/machine/upgrade_policy.rs b/crates/api-model/src/machine/upgrade_policy.rs index ed11e6a9a1..93dc1a9f8b 100644 --- a/crates/api-model/src/machine/upgrade_policy.rs +++ b/crates/api-model/src/machine/upgrade_policy.rs @@ -744,6 +744,9 @@ mod tests { ); } + // Total ordering over build versions. Each row pins the `cmp` result; the + // runner also asserts `partial_cmp` agrees (it must return `Some(cmp)`), so + // the PartialOrd impl is checked against Ord on every row. #[test] fn build_version_ordering() { struct Pair { @@ -754,7 +757,13 @@ mod tests { run = |Pair { left, right }| { let l = BuildVersion::try_from(left).unwrap(); let r = BuildVersion::try_from(right).unwrap(); - l.cmp(&r) + let ordering = l.cmp(&r); + assert_eq!( + l.partial_cmp(&r), + Some(ordering), + "partial_cmp must agree with cmp for {left} vs {right}", + ); + ordering }; "older date less than newer date" { Pair { @@ -828,28 +837,6 @@ mod tests { ); } - #[test] - fn build_version_partial_cmp_matches_cmp() { - value_scenarios!( - run = |(l, r)| { - BuildVersion::try_from(l) - .unwrap() - .partial_cmp(&BuildVersion::try_from(r).unwrap()) - }; - "less" { - ("v0.0.1", "v1.0.0") => Some(Ordering::Less), - } - - "greater" { - ("v1.0.0", "v0.0.1") => Some(Ordering::Greater), - } - - "equal" { - ("v1.0.0", "v1.0.0") => Some(Ordering::Equal), - } - ); - } - #[test] fn test_compare_versions() -> eyre::Result<()> { use rand::prelude::SliceRandom; diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index e8052192e5..32e0e51b5b 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -1562,7 +1562,7 @@ pub fn is_bluefield_model(model: &str) -> bool { #[cfg(test)] mod tests { use carbide_test_support::Outcome::*; - use carbide_test_support::{Case, check_cases, scenarios}; + use carbide_test_support::{Case, check_cases, scenarios, value_scenarios}; use super::*; use crate::firmware::FirmwareComponent; @@ -2096,17 +2096,63 @@ mod tests { assert_eq!(report.revision_id, None); } + // is_power_shelf identifies a power shelf either by a chassis id containing + // "powershelf" (manufacturer irrelevant) or by the generic "chassis" id paired + // with a Lite-On or Delta manufacturer. Any other id/manufacturer pairing is + // not a power shelf. Each row supplies a single chassis's id + manufacturer. #[test] - fn is_power_shelf_with_powershelf_chassis_id() { - let report = EndpointExplorationReport { - chassis: vec![Chassis { - id: "powershelf".to_string(), - manufacturer: Some("doesnt-matter-in-this-case".to_string()), - ..Default::default() - }], - ..Default::default() - }; - assert!(report.is_power_shelf()); + fn is_power_shelf_by_chassis_id_or_manufacturer() { + struct ChassisInput { + id: &'static str, + manufacturer: Option<&'static str>, + } + value_scenarios!( + run = |ChassisInput { id, manufacturer }| { + EndpointExplorationReport { + chassis: vec![Chassis { + id: id.to_string(), + manufacturer: manufacturer.map(str::to_string), + ..Default::default() + }], + ..Default::default() + } + .is_power_shelf() + }; + "powershelf chassis id (manufacturer irrelevant)" { + ChassisInput { + id: "powershelf", + manufacturer: Some("doesnt-matter-in-this-case"), + } => true, + } + + "generic chassis id + Lite-On manufacturer" { + ChassisInput { + id: "chassis", + manufacturer: Some("LITE-ON TECHNOLOGY CORP."), + } => true, + } + + "generic chassis id + Delta manufacturer" { + ChassisInput { + id: "chassis", + manufacturer: Some("DELTA"), + } => true, + } + + "generic chassis id + other manufacturer" { + ChassisInput { + id: "chassis", + manufacturer: Some("Dell Inc."), + } => false, + } + + "generic chassis id + no manufacturer" { + ChassisInput { + id: "chassis", + manufacturer: None, + } => false, + } + ); } /// `find_interface_id_for_mac` returns the Redfish interface id of the host @@ -2237,58 +2283,6 @@ mod tests { ); } - #[test] - fn is_power_shelf_with_chassis_id_and_liteon_manufacturer() { - let report = EndpointExplorationReport { - chassis: vec![Chassis { - id: "chassis".to_string(), - manufacturer: Some("LITE-ON TECHNOLOGY CORP.".to_string()), - ..Default::default() - }], - ..Default::default() - }; - assert!(report.is_power_shelf()); - } - - #[test] - fn is_power_shelf_with_chassis_id_and_delta_manufacturer() { - let report = EndpointExplorationReport { - chassis: vec![Chassis { - id: "chassis".to_string(), - manufacturer: Some("DELTA".to_string()), - ..Default::default() - }], - ..Default::default() - }; - assert!(report.is_power_shelf()); - } - - #[test] - fn is_power_shelf_with_generic_chassis_id_not_liteon() { - let report = EndpointExplorationReport { - chassis: vec![Chassis { - id: "chassis".to_string(), - manufacturer: Some("Dell Inc.".to_string()), - ..Default::default() - }], - ..Default::default() - }; - assert!(!report.is_power_shelf()); - } - - #[test] - fn is_power_shelf_with_no_manufacturer() { - let report = EndpointExplorationReport { - chassis: vec![Chassis { - id: "chassis".to_string(), - manufacturer: None, - ..Default::default() - }], - ..Default::default() - }; - assert!(!report.is_power_shelf()); - } - /// A `ComputerSystem` deserializes regardless of the `BaseMac` field: a valid /// value parses through, while an invalid, null, or missing one becomes `None`. /// Each row projects to the resulting `base_mac`. diff --git a/crates/rpc/src/model/tenant.rs b/crates/rpc/src/model/tenant.rs index b994c8ff93..77ccb6f736 100644 --- a/crates/rpc/src/model/tenant.rs +++ b/crates/rpc/src/model/tenant.rs @@ -894,58 +894,6 @@ mod tests { assert_eq!(config.subject_prefix, "spiffe://idp.other.example"); } - #[test] - fn identity_config_try_from_proto_rejects_when_no_allowlist_entry_matches() { - let allowlist = vec![ - "login.example.com".to_string(), - "*.tenant.example.net".to_string(), - ]; - let proto = rpc_forge::TenantIdentityConfig { - enabled: true, - issuer: "https://idp.other.example/".to_string(), - default_audience: "api".to_string(), - allowed_audiences: vec![], - token_ttl_sec: 3600, - subject_prefix: None, - rotate_key: false, - signing_key_overlap_sec: None, - }; - let bounds = IdentityConfigValidationBounds { - token_ttl_min_sec: 60, - token_ttl_max_sec: 86400, - algorithm: identity_config::SigningAlgorithm::Es256, - encryption_key_id: "test".parse().unwrap(), - trust_domain_allowlist: allowlist, - signing_key_overlap_max_sec: 604_800, - }; - let err = identity_config_try_from_proto(proto, &bounds).unwrap_err(); - assert!(err.0.contains("allowlist")); - } - - #[test] - fn identity_config_try_from_proto_rejects_overlap_when_not_rotating() { - let proto = rpc_forge::TenantIdentityConfig { - enabled: true, - issuer: "https://issuer.example.com".to_string(), - default_audience: "api".to_string(), - allowed_audiences: vec!["api".to_string()], - token_ttl_sec: 3600, - subject_prefix: None, - rotate_key: false, - signing_key_overlap_sec: Some(120), - }; - let bounds = IdentityConfigValidationBounds { - token_ttl_min_sec: 60, - token_ttl_max_sec: 86400, - algorithm: identity_config::SigningAlgorithm::Es256, - encryption_key_id: "test-master".parse().unwrap(), - trust_domain_allowlist: vec![], - signing_key_overlap_max_sec: 604_800, - }; - let err = identity_config_try_from_proto(proto, &bounds).unwrap_err(); - assert!(err.0.contains("signing_key_overlap_sec may only be set")); - } - // validate_identity_overlap_for_rotation: a missing overlap and an // overlap shorter than token_ttl_sec are rejected (with the listed // substring); a sufficient overlap is accepted (`None` want = Ok). diff --git a/crates/uuid/src/rack/mod.rs b/crates/uuid/src/rack/mod.rs index 428332948f..619b53ca08 100644 --- a/crates/uuid/src/rack/mod.rs +++ b/crates/uuid/src/rack/mod.rs @@ -288,7 +288,7 @@ pub enum RackIdParseError { #[cfg(test)] mod tests { use carbide_test_support::Outcome::*; - use carbide_test_support::{scenarios, value_scenarios}; + use carbide_test_support::scenarios; use super::*; @@ -297,38 +297,38 @@ mod tests { Empty, } - fn parse_rack_id(input: &str) -> Result { - RackId::from_str(input) - .map(|id| id.to_string()) - .map_err(|err| match err { - RackIdParseError::Empty => ParseFailure::Empty, - }) - } - - fn parse_rack_profile_id(input: &str) -> Result { - RackProfileId::from_str(input) - .map(|id| id.to_string()) - .map_err(|err| match err { - RackIdParseError::Empty => ParseFailure::Empty, - }) - } + // RackId and RackProfileId are parallel `serde(transparent)` newtypes with the + // same `FromStr` (rejecting only the empty string with `RackIdParseError::Empty`) + // and the same JSON behavior. The parse and serde tables run one generic helper + // over both types, so each type only supplies its own distinct inputs. - fn deserialize_rack_id(input: &str) -> Result { - serde_json::from_str::(input) + // Parse a string into the newtype `T`, projecting success to the recovered inner + // string and the one parse error to `ParseFailure` so rows stay comparable. + fn parse_as(input: &str) -> Result + where + T: FromStr + Display, + { + T::from_str(input) .map(|id| id.to_string()) - .map_err(|_| ()) + .map_err(|RackIdParseError::Empty| ParseFailure::Empty) } - fn deserialize_rack_profile_id(input: &str) -> Result { - serde_json::from_str::(input) + // Deserialize JSON into the newtype `T`, projecting success to the recovered + // inner string. `serde_json::Error` is not `PartialEq`, so rejected rows use + // `Fails` with the error discarded. + fn deserialize_as(input: &str) -> Result + where + T: serde::de::DeserializeOwned + Display, + { + serde_json::from_str::(input) .map(|id| id.to_string()) .map_err(|_| ()) } #[test] - fn test_rack_id_parse_cases() { + fn rack_id_types_parse() { scenarios!( - run = parse_rack_id; + run = parse_as::; "legacy ps100-encoded rack ID" { "ps100ht038bg3qsho433vkg684heguv282qaggmrsh2ugn1qk096n2c6hcg" => Yields( "ps100ht038bg3qsho433vkg684heguv282qaggmrsh2ugn1qk096n2c6hcg".to_string(), @@ -351,70 +351,9 @@ mod tests { "" => FailsWith(ParseFailure::Empty), } ); - } - - #[test] - fn test_rack_id_conversions() { - value_scenarios!( - run = |rack_id| { - ( - rack_id.as_str().to_string(), - rack_id.to_string(), - rack_id.as_ref().to_string(), - ) - }; - "new" { - RackId::new("test-rack") => ( - "test-rack".to_string(), - "test-rack".to_string(), - "test-rack".to_string(), - ), - } - - "from str" { - RackId::from("another-rack") => ( - "another-rack".to_string(), - "another-rack".to_string(), - "another-rack".to_string(), - ), - } - - "from string" { - RackId::from(String::from("string-rack")) => ( - "string-rack".to_string(), - "string-rack".to_string(), - "string-rack".to_string(), - ), - } - ); - } - - #[test] - fn test_rack_id_serde_cases() { - scenarios!( - run = deserialize_rack_id; - "valid string" { - "\"my-custom-rack\"" => Yields("my-custom-rack".to_string()), - } - - "empty string" { - "\"\"" => Yields(String::new()), - } - - "non-string JSON" { - "42" => Fails, - } - ); - let serialized = serde_json::to_string(&RackId::new("my-custom-rack")) - .expect("failed to serialize rack ID"); - assert_eq!(serialized, "\"my-custom-rack\""); - } - - #[test] - fn test_rack_profile_id_parse_cases() { scenarios!( - run = parse_rack_profile_id; + run = parse_as::; "rack profile name" { "NVL72" => Yields("NVL72".to_string()), } @@ -430,45 +369,24 @@ mod tests { } #[test] - fn test_rack_profile_id_conversions() { - value_scenarios!( - run = |profile_id| { - ( - profile_id.as_str().to_string(), - profile_id.to_string(), - profile_id.as_ref().to_string(), - ) - }; - "new" { - RackProfileId::new("NVL72") => ( - "NVL72".to_string(), - "NVL72".to_string(), - "NVL72".to_string(), - ), + fn rack_id_types_serde() { + scenarios!( + run = deserialize_as::; + "valid string" { + "\"my-custom-rack\"" => Yields("my-custom-rack".to_string()), } - "from str" { - RackProfileId::from("NVL36") => ( - "NVL36".to_string(), - "NVL36".to_string(), - "NVL36".to_string(), - ), + "empty string" { + "\"\"" => Yields(String::new()), } - "from string" { - RackProfileId::from(String::from("GB200")) => ( - "GB200".to_string(), - "GB200".to_string(), - "GB200".to_string(), - ), + "non-string JSON" { + "42" => Fails, } ); - } - #[test] - fn test_rack_profile_id_serde_cases() { scenarios!( - run = deserialize_rack_profile_id; + run = deserialize_as::; "valid string" { "\"NVL72\"" => Yields("NVL72".to_string()), } @@ -482,6 +400,11 @@ mod tests { } ); + // serde(transparent) serializes each newtype as the bare inner string. + let serialized = serde_json::to_string(&RackId::new("my-custom-rack")) + .expect("failed to serialize rack ID"); + assert_eq!(serialized, "\"my-custom-rack\""); + let serialized = serde_json::to_string(&RackProfileId::new("NVL72")) .expect("failed to serialize rack profile ID"); assert_eq!(serialized, "\"NVL72\"");