|
| 1 | +//! Property-based tests for configuration handling |
| 2 | +//! |
| 3 | +//! Tests that configuration merging, validation, and type resolution work correctly. |
| 4 | +
|
| 5 | +use novalyn_core::config::{SemverImpact, TypeConfigResolved, default_types}; |
| 6 | +use proptest::prelude::*; |
| 7 | + |
| 8 | +// Strategy for generating valid commit type keys |
| 9 | +fn valid_type_key() -> impl Strategy<Value = String> { |
| 10 | + prop::string::string_regex("[a-z]{2,15}").unwrap() |
| 11 | +} |
| 12 | + |
| 13 | +// Strategy for generating emoji strings |
| 14 | +fn emoji_string() -> impl Strategy<Value = String> { |
| 15 | + prop::sample::select(vec![ |
| 16 | + "✨".to_string(), |
| 17 | + "🐞".to_string(), |
| 18 | + "⚡️".to_string(), |
| 19 | + "📚".to_string(), |
| 20 | + "🛠".to_string(), |
| 21 | + "🎨".to_string(), |
| 22 | + "🧪".to_string(), |
| 23 | + "📦".to_string(), |
| 24 | + "👷".to_string(), |
| 25 | + "🧹".to_string(), |
| 26 | + "⏪".to_string(), |
| 27 | + ]) |
| 28 | +} |
| 29 | + |
| 30 | +// Strategy for generating title strings |
| 31 | +fn title_string() -> impl Strategy<Value = String> { |
| 32 | + prop::string::string_regex("[A-Z][a-zA-Z ]{2,30}").unwrap() |
| 33 | +} |
| 34 | + |
| 35 | +// Strategy for generating SemverImpact |
| 36 | +fn semver_impact() -> impl Strategy<Value = SemverImpact> { |
| 37 | + prop::sample::select(vec![ |
| 38 | + SemverImpact::Major, |
| 39 | + SemverImpact::Minor, |
| 40 | + SemverImpact::Patch, |
| 41 | + SemverImpact::None, |
| 42 | + ]) |
| 43 | +} |
| 44 | + |
| 45 | +proptest! { |
| 46 | + /// Test that TypeConfigResolved can be created with various valid inputs |
| 47 | + #[test] |
| 48 | + fn type_config_creation( |
| 49 | + key in valid_type_key(), |
| 50 | + title in title_string(), |
| 51 | + emoji in emoji_string(), |
| 52 | + semver in semver_impact(), |
| 53 | + enabled in proptest::bool::ANY, |
| 54 | + ) { |
| 55 | + let config = TypeConfigResolved { |
| 56 | + key: key.clone().into(), |
| 57 | + title: title.into(), |
| 58 | + emoji: emoji.into(), |
| 59 | + semver, |
| 60 | + enabled, |
| 61 | + }; |
| 62 | + |
| 63 | + assert_eq!(config.key.as_str(), key); |
| 64 | + assert_eq!(config.enabled, enabled); |
| 65 | + assert_eq!(config.semver, semver); |
| 66 | + } |
| 67 | + |
| 68 | + /// Test that SemverImpact string parsing is case-sensitive |
| 69 | + #[test] |
| 70 | + fn semver_impact_string_parsing( |
| 71 | + valid in prop::sample::select(vec!["major", "minor", "patch", "none"]) |
| 72 | + ) { |
| 73 | + let parsed = SemverImpact::parse(valid); |
| 74 | + assert!(parsed.is_some(), "Should parse valid semver impact: {}", valid); |
| 75 | + } |
| 76 | + |
| 77 | + /// Test that invalid SemverImpact strings return None |
| 78 | + #[test] |
| 79 | + fn semver_impact_invalid_strings( |
| 80 | + invalid in prop::string::string_regex("[A-Z]{1,10}").unwrap() |
| 81 | + ) { |
| 82 | + // Uppercase versions should not parse |
| 83 | + let parsed = SemverImpact::parse(&invalid); |
| 84 | + assert!(parsed.is_none(), "Should not parse: {}", invalid); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +// Regular unit tests for default configuration |
| 89 | +#[cfg(test)] |
| 90 | +mod default_config_tests { |
| 91 | + use super::*; |
| 92 | + |
| 93 | + #[test] |
| 94 | + fn default_types_order_consistent() { |
| 95 | + let types = default_types(); |
| 96 | + |
| 97 | + // Should always have the standard types in order |
| 98 | + assert!(types.len() >= 11); // At least the standard 11 types |
| 99 | + |
| 100 | + // First few should be feat, fix, perf in that order |
| 101 | + assert_eq!(types[0].key.as_str(), "feat"); |
| 102 | + assert_eq!(types[1].key.as_str(), "fix"); |
| 103 | + assert_eq!(types[2].key.as_str(), "perf"); |
| 104 | + } |
| 105 | + |
| 106 | + #[test] |
| 107 | + fn default_types_have_all_fields() { |
| 108 | + let types = default_types(); |
| 109 | + |
| 110 | + for t in &types { |
| 111 | + assert!(!t.key.is_empty(), "Type key should not be empty"); |
| 112 | + assert!(!t.title.is_empty(), "Type title should not be empty"); |
| 113 | + assert!(!t.emoji.is_empty(), "Type emoji should not be empty"); |
| 114 | + assert!(t.enabled, "Default types should be enabled"); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + #[test] |
| 119 | + fn default_types_keys_unique() { |
| 120 | + let types = default_types(); |
| 121 | + let mut seen = std::collections::HashSet::new(); |
| 122 | + |
| 123 | + for t in &types { |
| 124 | + assert!(seen.insert(t.key.clone()), "Duplicate key: {}", t.key); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + #[test] |
| 129 | + fn default_types_semver_sensible() { |
| 130 | + let types = default_types(); |
| 131 | + |
| 132 | + // feat should always be Minor |
| 133 | + let feat = types.iter().find(|t| t.key.as_str() == "feat").unwrap(); |
| 134 | + assert_eq!(feat.semver, SemverImpact::Minor); |
| 135 | + |
| 136 | + // fix should always be Patch |
| 137 | + let fix = types.iter().find(|t| t.key.as_str() == "fix").unwrap(); |
| 138 | + assert_eq!(fix.semver, SemverImpact::Patch); |
| 139 | + |
| 140 | + // docs should always be None |
| 141 | + let docs = types.iter().find(|t| t.key.as_str() == "docs").unwrap(); |
| 142 | + assert_eq!(docs.semver, SemverImpact::None); |
| 143 | + } |
| 144 | + |
| 145 | + #[test] |
| 146 | + fn default_types_naming_conventions() { |
| 147 | + let types = default_types(); |
| 148 | + |
| 149 | + for t in &types { |
| 150 | + // Keys should be lowercase alphabetic |
| 151 | + assert!( |
| 152 | + t.key.chars().all(|c| c.is_ascii_lowercase()), |
| 153 | + "Key should be lowercase: {}", |
| 154 | + t.key |
| 155 | + ); |
| 156 | + |
| 157 | + // Titles should start with uppercase |
| 158 | + assert!( |
| 159 | + t.title.chars().next().unwrap().is_ascii_uppercase(), |
| 160 | + "Title should start with uppercase: {}", |
| 161 | + t.title |
| 162 | + ); |
| 163 | + } |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +// Additional unit tests for specific functionality |
| 168 | +#[cfg(test)] |
| 169 | +mod config_specifics { |
| 170 | + use super::*; |
| 171 | + |
| 172 | + #[test] |
| 173 | + fn test_semver_impact_ordering() { |
| 174 | + // Major > Minor > Patch > None in severity |
| 175 | + let impacts = [ |
| 176 | + SemverImpact::None, |
| 177 | + SemverImpact::Patch, |
| 178 | + SemverImpact::Minor, |
| 179 | + SemverImpact::Major, |
| 180 | + ]; |
| 181 | + |
| 182 | + // Just verify they are all distinct |
| 183 | + for i in 0..impacts.len() { |
| 184 | + for j in 0..impacts.len() { |
| 185 | + if i == j { |
| 186 | + assert_eq!(impacts[i], impacts[j]); |
| 187 | + } else { |
| 188 | + assert_ne!(impacts[i], impacts[j]); |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn test_default_types_completeness() { |
| 196 | + let types = default_types(); |
| 197 | + let expected = [ |
| 198 | + "feat", "fix", "perf", "docs", "refactor", "style", "test", "build", "ci", "chore", |
| 199 | + "revert", |
| 200 | + ]; |
| 201 | + |
| 202 | + for expected_key in &expected { |
| 203 | + assert!( |
| 204 | + types.iter().any(|t| t.key.as_str() == *expected_key), |
| 205 | + "Missing expected type: {}", |
| 206 | + expected_key |
| 207 | + ); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + #[test] |
| 212 | + fn test_semver_impact_from_str() { |
| 213 | + assert_eq!(SemverImpact::parse("major"), Some(SemverImpact::Major)); |
| 214 | + assert_eq!(SemverImpact::parse("minor"), Some(SemverImpact::Minor)); |
| 215 | + assert_eq!(SemverImpact::parse("patch"), Some(SemverImpact::Patch)); |
| 216 | + assert_eq!(SemverImpact::parse("none"), Some(SemverImpact::None)); |
| 217 | + |
| 218 | + // Invalid inputs |
| 219 | + assert_eq!(SemverImpact::parse("Major"), None); |
| 220 | + assert_eq!(SemverImpact::parse("MAJOR"), None); |
| 221 | + assert_eq!(SemverImpact::parse(""), None); |
| 222 | + assert_eq!(SemverImpact::parse("invalid"), None); |
| 223 | + } |
| 224 | + |
| 225 | + #[test] |
| 226 | + fn test_type_config_enabled_flag() { |
| 227 | + let enabled = TypeConfigResolved { |
| 228 | + key: "test".into(), |
| 229 | + title: "Test".into(), |
| 230 | + emoji: "🧪".into(), |
| 231 | + semver: SemverImpact::None, |
| 232 | + enabled: true, |
| 233 | + }; |
| 234 | + assert!(enabled.enabled); |
| 235 | + |
| 236 | + let disabled = TypeConfigResolved { |
| 237 | + key: "test".into(), |
| 238 | + title: "Test".into(), |
| 239 | + emoji: "🧪".into(), |
| 240 | + semver: SemverImpact::None, |
| 241 | + enabled: false, |
| 242 | + }; |
| 243 | + assert!(!disabled.enabled); |
| 244 | + } |
| 245 | +} |
0 commit comments