use proptest::prelude::*;
pub fn semver_strategy() -> impl Strategy<Value = String> {
(0u32..100, 0u32..100, 0u32..100)
.prop_map(|(major, minor, patch)| format!("{}.{}.{}", major, minor, patch))
}
pub fn semver_with_prerelease_strategy() -> impl Strategy<Value = String> {
(0u32..100, 0u32..100, 0u32..100, prop::option::of("[a-z]{3,8}"), prop::option::of(0u32..100))
.prop_map(|(major, minor, patch, pre, pre_num)| {
let mut version = format!("{}.{}.{}", major, minor, patch);
if let Some(pre_tag) = pre {
version.push('-');
version.push_str(&pre_tag);
if let Some(num) = pre_num {
version.push('.');
version.push_str(&num.to_string());
}
}
version
})
}
#[allow(dead_code)]
pub fn semver_with_build_strategy() -> impl Strategy<Value = String> {
(0u32..100, 0u32..100, 0u32..100, prop::option::of(0u32..1000)).prop_map(
|(major, minor, patch, build)| {
let mut version = format!("{}.{}.{}", major, minor, patch);
if let Some(build_num) = build {
version.push_str(&format!("+build.{}", build_num));
}
version
},
)
}
pub fn package_name_strategy() -> impl Strategy<Value = String> {
let simple_name = "[a-z][a-z0-9-_]{2,30}";
let scoped_name = "@[a-z][a-z0-9-]{2,20}/[a-z][a-z0-9-_]{2,30}";
prop::string::string_regex(&format!("({}|{})", simple_name, scoped_name)).expect("valid regex")
}
pub fn commit_type_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"feat".to_string(),
"fix".to_string(),
"docs".to_string(),
"style".to_string(),
"refactor".to_string(),
"perf".to_string(),
"test".to_string(),
"build".to_string(),
"ci".to_string(),
"chore".to_string(),
"revert".to_string(),
])
}
pub fn commit_scope_strategy() -> impl Strategy<Value = Option<String>> {
prop::option::of(prop::string::string_regex("[a-z][a-z0-9-]{2,15}").expect("valid regex"))
}
pub fn commit_description_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-z][a-z0-9 ,.-]{10,80}").expect("valid regex")
}
pub fn conventional_commit_strategy() -> impl Strategy<Value = String> {
(
commit_type_strategy(),
commit_scope_strategy(),
commit_description_strategy(),
prop::bool::ANY,
)
.prop_map(|(commit_type, scope, description, breaking)| {
let breaking_marker = if breaking { "!" } else { "" };
match scope {
Some(s) => {
format!("{}({}){}: {}", commit_type, s, breaking_marker, description)
}
None => format!("{}{}: {}", commit_type, breaking_marker, description),
}
})
}
pub fn file_path_strategy() -> impl Strategy<Value = String> {
prop::collection::vec("[a-z][a-z0-9-]{2,15}", 1..5).prop_map(|parts| {
let mut path = parts.join("/");
path.push_str(".txt");
path
})
}
#[allow(dead_code)]
pub fn version_bump_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"major".to_string(),
"minor".to_string(),
"patch".to_string(),
"none".to_string(),
])
}
pub fn environment_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"development".to_string(),
"staging".to_string(),
"production".to_string(),
"test".to_string(),
"qa".to_string(),
])
}
pub fn environment_list_strategy() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec(environment_strategy(), 1..4)
}
pub fn commit_hash_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-f0-9]{40}").expect("valid regex")
}
pub fn short_commit_hash_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-f0-9]{7}").expect("valid regex")
}
#[allow(dead_code)]
pub fn author_name_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[A-Z][a-z]+ [A-Z][a-z]+").expect("valid regex")
}
pub fn author_email_strategy() -> impl Strategy<Value = String> {
(
prop::string::string_regex("[a-z][a-z0-9]{2,10}").expect("valid regex"),
prop::string::string_regex("[a-z]{3,10}").expect("valid regex"),
prop::string::string_regex("[a-z]{2,5}").expect("valid regex"),
)
.prop_map(|(user, domain, tld)| format!("{}@{}.{}", user, domain, tld))
}
#[allow(dead_code)]
pub fn branch_name_strategy() -> impl Strategy<Value = String> {
proptest::prop_oneof![
prop::sample::select(vec!["main".to_string(), "develop".to_string(), "master".to_string()]),
prop::string::string_regex("(feature|bugfix|hotfix)/[a-z][a-z0-9-]{5,20}")
.expect("valid regex"),
]
}
#[allow(dead_code)]
pub fn changeset_id_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}")
.expect("valid regex")
}
#[allow(dead_code)]
pub fn version_spec_strategy() -> impl Strategy<Value = String> {
(prop::sample::select(vec!["^", "~", ">=", "<=", "="]), 0u32..100, 0u32..100, 0u32..100)
.prop_map(|(prefix, major, minor, patch)| {
format!("{}{}.{}.{}", prefix, major, minor, patch)
})
}
#[cfg(test)]
mod tests {
use super::*;
use semver::Version;
proptest! {
#[test]
fn test_semver_strategy_generates_valid_versions(version in semver_strategy()) {
assert!(Version::parse(&version).is_ok());
}
#[test]
fn test_semver_with_prerelease_is_parseable(version in semver_with_prerelease_strategy()) {
assert!(Version::parse(&version).is_ok());
}
#[test]
fn test_package_names_are_valid(name in package_name_strategy()) {
assert!(!name.is_empty());
assert!(name.len() >= 3);
assert_eq!(name.to_lowercase(), name);
}
#[test]
fn test_conventional_commits_have_colon(message in conventional_commit_strategy()) {
assert!(message.contains(':'));
}
#[test]
fn test_commit_hashes_are_40_chars(hash in commit_hash_strategy()) {
assert_eq!(hash.len(), 40);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_short_hashes_are_7_chars(hash in short_commit_hash_strategy()) {
assert_eq!(hash.len(), 7);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_author_emails_have_at_sign(email in author_email_strategy()) {
assert!(email.contains('@'));
assert!(email.contains('.'));
}
#[test]
fn test_file_paths_have_extension(path in file_path_strategy()) {
assert!(path.ends_with(".txt"));
}
#[test]
fn test_environment_list_not_empty(envs in environment_list_strategy()) {
assert!(!envs.is_empty());
}
}
}