use proptest::prelude::*;
use super::{
FlakeRef, FlakeRefType, GitForge, GitForgePlatform, LocationParameters, RefLocation,
ResourceType, ResourceUrl, TransportLayer,
};
fn ref_string_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_.\\-]{0,15}".prop_filter(
"ref must not be 40- or 64-hex (would classify as rev)",
|s: &String| !super::validators::looks_like_rev(s),
)
}
fn rev_string_strategy() -> impl Strategy<Value = String> {
prop_oneof!["[0-9a-f]{40}", "[0-9a-f]{64}"]
}
fn ref_location_strategy() -> impl Strategy<Value = RefLocation> {
prop_oneof![
Just(RefLocation::PathComponent),
Just(RefLocation::QueryParameter),
]
}
fn opt_ref_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), ref_string_strategy().prop_map(Some)]
}
fn opt_rev_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![Just(None), rev_string_strategy().prop_map(Some)]
}
fn platform_strategy() -> impl Strategy<Value = GitForgePlatform> {
prop_oneof![
Just(GitForgePlatform::GitHub),
Just(GitForgePlatform::GitLab),
Just(GitForgePlatform::SourceHut),
]
}
fn owner_or_repo_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_-]{0,15}"
}
fn gitlab_owner_strategy() -> impl Strategy<Value = String> {
proptest::collection::vec("[a-zA-Z][a-zA-Z0-9_-]{0,7}", 1..=4).prop_map(|segs| segs.join("/"))
}
fn id_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_-]{0,15}"
}
fn normalise_location(
ref_: Option<String>,
rev: Option<String>,
location: RefLocation,
) -> (Option<String>, Option<String>, RefLocation) {
if ref_.is_none() && rev.is_none() {
(ref_, rev, RefLocation::PathComponent)
} else {
(ref_, rev, location)
}
}
fn git_forge_strategy() -> impl Strategy<Value = GitForge> {
(
platform_strategy(),
owner_or_repo_strategy(),
gitlab_owner_strategy(),
owner_or_repo_strategy(),
opt_ref_strategy(),
opt_rev_strategy(),
ref_location_strategy(),
)
.prop_map(
|(platform, plain_owner, gitlab_owner, repo, ref_, rev, location)| {
let owner = match platform {
GitForgePlatform::GitLab => gitlab_owner,
_ => plain_owner,
};
let (ref_, rev) = if ref_.is_some() && rev.is_some() {
(ref_, None)
} else {
(ref_, rev)
};
let (ref_, rev, location) = normalise_location(ref_, rev, location);
GitForge {
platform,
owner,
repo,
ref_,
rev,
location,
}
},
)
}
fn indirect_strategy() -> impl Strategy<Value = FlakeRefType> {
(
id_strategy(),
opt_ref_strategy(),
opt_rev_strategy(),
ref_location_strategy(),
)
.prop_map(|(id, ref_, rev, location)| {
let (ref_, rev, location) = normalise_location(ref_, rev, location);
FlakeRefType::Indirect {
id,
ref_,
rev,
location,
}
})
}
fn path_strategy() -> impl Strategy<Value = FlakeRefType> {
let body = prop_oneof![
"/[a-zA-Z0-9._-][a-zA-Z0-9._/-]{0,15}",
r"\.\.?/[a-zA-Z0-9._/-]{1,16}",
Just(".".to_string()),
Just("..".to_string()),
];
(body, opt_rev_strategy()).prop_map(|(path, rev)| FlakeRefType::Path { path, rev })
}
fn transport_strategy() -> impl Strategy<Value = TransportLayer> {
prop_oneof![
Just(TransportLayer::Http),
Just(TransportLayer::Https),
Just(TransportLayer::Ssh),
Just(TransportLayer::File),
]
}
fn resource_location_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9.]{0,15}/[a-zA-Z0-9._\\-/]{1,32}"
}
fn tarball_location_strategy() -> impl Strategy<Value = String> {
(
"[a-z][a-z0-9.]{0,15}/[a-zA-Z0-9._\\-/]{1,16}",
prop_oneof![
Just(".zip"),
Just(".tar"),
Just(".tgz"),
Just(".tar.gz"),
Just(".tar.xz"),
Just(".tar.bz2"),
Just(".tar.zst"),
],
)
.prop_map(|(base, ext)| format!("{base}{ext}"))
}
fn file_location_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9.]{0,15}/[a-zA-Z0-9._\\-/]{1,32}"
.prop_filter("must not collide with a tarball extension", |s: &String| {
!crate::parser::is_tarball(s)
})
}
fn curl_transport_strategy() -> impl Strategy<Value = TransportLayer> {
prop_oneof![Just(TransportLayer::Http), Just(TransportLayer::Https)]
}
fn make_resource(
res_type: ResourceType,
location: String,
transport_type: Option<TransportLayer>,
ref_: Option<String>,
rev: Option<String>,
) -> FlakeRefType {
let ref_location = if ref_.is_some() || rev.is_some() {
RefLocation::QueryParameter
} else {
RefLocation::PathComponent
};
FlakeRefType::Resource(ResourceUrl {
res_type,
location,
transport_type,
ref_,
rev,
ref_location,
})
}
fn git_resource_strategy() -> impl Strategy<Value = FlakeRefType> {
(
resource_location_strategy(),
prop_oneof![
Just(None::<TransportLayer>),
transport_strategy().prop_map(Some),
],
opt_ref_strategy(),
opt_rev_strategy(),
)
.prop_map(|(location, transport_type, ref_, rev)| {
make_resource(ResourceType::Git, location, transport_type, ref_, rev)
})
}
fn mercurial_resource_strategy() -> impl Strategy<Value = FlakeRefType> {
(
resource_location_strategy(),
transport_strategy(),
opt_ref_strategy(),
opt_rev_strategy(),
)
.prop_map(|(location, transport_type, ref_, rev)| {
make_resource(
ResourceType::Mercurial,
location,
Some(transport_type),
ref_,
rev,
)
})
}
fn tarball_resource_strategy() -> impl Strategy<Value = FlakeRefType> {
(
tarball_location_strategy(),
curl_transport_strategy(),
opt_rev_strategy(),
)
.prop_map(|(location, transport, rev)| {
make_resource(ResourceType::Tarball, location, Some(transport), None, rev)
})
}
fn file_resource_strategy() -> impl Strategy<Value = FlakeRefType> {
(
file_location_strategy(),
curl_transport_strategy(),
opt_rev_strategy(),
)
.prop_map(|(location, transport, rev)| {
make_resource(ResourceType::File, location, Some(transport), None, rev)
})
}
fn resource_strategy() -> impl Strategy<Value = FlakeRefType> {
prop_oneof![
git_resource_strategy(),
mercurial_resource_strategy(),
tarball_resource_strategy(),
file_resource_strategy(),
]
}
fn kind_strategy() -> impl Strategy<Value = FlakeRefType> {
prop_oneof![
git_forge_strategy().prop_map(FlakeRefType::GitForge),
indirect_strategy(),
path_strategy(),
resource_strategy(),
]
}
fn percent_encoded_value_strategy() -> impl Strategy<Value = String> {
prop_oneof![
4 => "[a-zA-Z0-9._\\-]{0,16}",
1 => proptest::string::string_regex("[a-zA-Z0-9 %&=#+;<>Öö\u{e9}\u{65e5}]{1,16}")
.expect("regex must compile"),
]
}
fn fragment_strategy() -> impl Strategy<Value = Option<String>> {
prop_oneof![
Just(None),
"[a-zA-Z][a-zA-Z0-9._\\-]{0,15}".prop_map(Some),
proptest::string::string_regex("[a-zA-Z0-9 %&=#+;<>:@/?Öö\u{e9}\u{65e5}]{1,16}")
.expect("regex must compile")
.prop_map(Some),
]
}
type GitTypedParams = (
Option<bool>,
Option<bool>,
Option<bool>,
Option<bool>,
Option<String>,
Option<String>,
Option<String>,
);
fn git_typed_params_strategy() -> impl Strategy<Value = GitTypedParams> {
let key_value = "[a-zA-Z0-9._\\-]{1,16}";
(
prop::option::of(any::<bool>()),
prop::option::of(any::<bool>()),
prop::option::of(any::<bool>()),
prop::option::of(any::<bool>()),
prop::option::of(key_value),
prop::option::of(key_value),
prop::option::of(key_value),
)
}
fn location_parameters_strategy() -> impl Strategy<Value = LocationParameters> {
(
prop::option::of(percent_encoded_value_strategy()),
prop::option::of("[a-zA-Z0-9.\\-]{1,16}"),
prop::option::of("sha256-[a-zA-Z0-9_-]{8,32}"),
prop::option::of("[0-9]{1,12}"),
prop::option::of("[0-9]{1,8}"),
prop::option::of(any::<bool>()),
prop::option::of(any::<bool>()),
git_typed_params_strategy(),
)
.prop_map(
|(dir, host, nar_hash, last_modified, rev_count, submodules, shallow, git_typed)| {
let mut params = LocationParameters::default();
if let Some(d) = dir {
params.set_dir(Some(d));
}
if let Some(h) = host {
params.set_host(Some(h));
}
if let Some(n) = nar_hash {
params.set_nar_hash(Some(n));
}
if let Some(lm) = last_modified {
params.set_last_modified(Some(lm));
}
if let Some(rc) = rev_count {
params.set_rev_count(Some(rc));
}
if let Some(s) = submodules {
params.set_submodules(Some(s));
}
if let Some(s) = shallow {
params.set_shallow(Some(s));
}
let (lfs, export_ignore, all_refs, verify_commit, keytype, public_key, public_keys) =
git_typed;
params.set_lfs(lfs);
params.set_export_ignore(export_ignore);
params.set_all_refs(all_refs);
params.set_verify_commit(verify_commit);
params.set_keytype(keytype);
params.set_public_key(public_key);
params.set_public_keys(public_keys);
params
},
)
}
fn flake_ref_strategy() -> impl Strategy<Value = FlakeRef> {
(
kind_strategy(),
fragment_strategy(),
location_parameters_strategy(),
)
.prop_map(|(kind, fragment, params)| {
FlakeRef::new(kind)
.with_fragment(fragment)
.with_params(params)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1024))]
#[test]
fn roundtrip_via_string(flake_ref in flake_ref_strategy()) {
let s = flake_ref.to_string();
let parsed: FlakeRef = s.parse().expect("Display output failed to parse");
prop_assert_eq!(flake_ref, parsed);
}
#[test]
fn display_is_canonical(flake_ref in flake_ref_strategy()) {
let s1 = flake_ref.to_string();
let parsed: FlakeRef = s1.parse().expect("Display output failed to parse");
let s2 = parsed.to_string();
prop_assert_eq!(s1, s2);
}
}