use proptest::prelude::*;
use sealmap_model::{Descriptor, Package, Suffix, SymbolId, Version};
fn tricky() -> impl Strategy<Value = String> {
let ch = prop_oneof![
Just('a'),
Just('Z'),
Just('0'),
Just('_'),
Just('+'),
Just('-'),
Just('$'),
Just('`'),
Just(' '),
Just(':'),
Just('/'),
Just('#'),
Just('.'),
Just('('),
Just(')'),
Just('['),
Just(']'),
Just('!'),
Just('<'),
Just('?'),
Just('\n'),
Just('é'),
Just('ß'),
];
proptest::collection::vec(ch, 0..6).prop_map(|v| v.into_iter().collect())
}
fn field() -> impl Strategy<Value = String> {
tricky().prop_filter_map("valid package field", |s| {
let ok = !s.is_empty() && s != "." && !s.starts_with(' ') && !s.ends_with(' ') && !s.contains('\n');
ok.then_some(s)
})
}
fn package() -> impl Strategy<Value = Package> {
let manager = prop_oneof![Just("cargo"), Just("npm"), Just("a-1"), Just("x")];
let version = prop_oneof![Just(Version::Current), field().prop_map(Version::Release)];
(manager, field(), version).prop_map(|(m, n, v)| Package::new(m, n, v).expect("generated fields are valid"))
}
fn descriptor() -> impl Strategy<Value = Descriptor> {
let disambiguator = prop_oneof![Just(None), Just(Some("1".to_owned())), Just(Some("a+b$".to_owned()))];
let suffix = prop_oneof![
Just(Suffix::Namespace),
Just(Suffix::Type),
Just(Suffix::Term),
disambiguator.prop_map(|disambiguator| Suffix::Method { disambiguator }),
Just(Suffix::TypeParameter),
Just(Suffix::Parameter),
Just(Suffix::Meta),
Just(Suffix::Macro),
];
(tricky(), suffix).prop_map(|(n, s)| Descriptor::new(n, s).expect("generated disambiguators are valid"))
}
fn symbol_id() -> impl Strategy<Value = SymbolId> {
prop_oneof![
4 => (package(), proptest::collection::vec(descriptor(), 0..5)).prop_map(|(p, d)| SymbolId::global(p, d)),
1 => proptest::collection::vec(tricky(), 1..4).prop_map(|s| SymbolId::path(s).expect("non-empty")),
1 => tricky().prop_map(SymbolId::unresolved),
]
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn parse_inverts_print(id in symbol_id()) {
let text = id.to_string();
prop_assert_eq!(SymbolId::parse(&text), Ok(id), "text: {:?}", text);
}
#[test]
fn print_is_injective(a in symbol_id(), b in symbol_id()) {
if a != b {
prop_assert_ne!(a.to_string(), b.to_string());
}
}
#[test]
fn parent_is_a_prefix(id in symbol_id()) {
if let Some(p) = id.parent() {
prop_assert!(p < id, "{} !< {}", p, id);
}
}
}
#[test]
fn historical_collisions_stay_distinct() {
let pkg = |n: &str| Package::current("cargo", n).unwrap();
let pairs = [
(
SymbolId::global(pkg("a"), vec![Descriptor::namespace("b__c")]),
SymbolId::global(pkg("a"), vec![Descriptor::namespace("b"), Descriptor::namespace("c")]),
),
(
SymbolId::global(pkg("a"), vec![Descriptor::namespace("foo")]),
SymbolId::global(pkg("a"), vec![Descriptor::method("foo")]),
),
(
SymbolId::global(pkg("a"), vec![Descriptor::r#type("T"), Descriptor::type_parameter("From<u8>")]),
SymbolId::global(pkg("a"), vec![Descriptor::r#type("T"), Descriptor::type_parameter("From_u8_")]),
),
(SymbolId::path(["a", "b"]).unwrap(), SymbolId::path(["a::b"]).unwrap()),
(SymbolId::unresolved("x"), SymbolId::path(["?", "x"]).unwrap()),
];
for (x, y) in pairs {
assert_ne!(x.to_string(), y.to_string());
}
assert!(Package::current("cargo", "a ").is_err());
assert!(Package::current("extern", "a").is_err());
}
proptest! {
#[test]
fn accepted_text_is_canonical(text in "sym:(cargo|extern|\\?) [a-c` .:/#()\\[\\]!+]{0,12}") {
if let Ok(id) = SymbolId::parse(&text) {
prop_assert_eq!(id.to_string(), text);
}
}
}
fn structural(id: &SymbolId) -> (Vec<String>, Option<SymbolId>) {
if let Some(p) = id.package() {
let mut d = id.descriptors();
let names = std::iter::once(p.name().to_owned()).chain(d.iter().map(|d| d.name().to_owned())).collect();
let parent = d.pop().map(|_| {
while d.last().is_some_and(|d| d.suffix() == &Suffix::TypeParameter) {
d.pop();
}
SymbolId::global(p.clone(), d)
});
return (names, parent);
}
if let Some(s) = id.segments() {
let parent = (s.len() > 1).then(|| SymbolId::path(s[..s.len() - 1].to_vec()).unwrap());
return (s, parent);
}
(vec![id.name().into_owned()], None)
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn scanner_agrees_with_structure(id in symbol_id(), d in descriptor()) {
let (names, parent) = structural(&id);
let got: Vec<String> = id.names().into_iter().map(|n| n.into_owned()).collect();
prop_assert_eq!(&got, &names);
prop_assert_eq!(id.name().into_owned(), names.last().cloned().unwrap_or_default());
prop_assert_eq!(id.parent(), parent);
if id.is_global() {
prop_assert_eq!(id.root().map(|r| r.into_owned()), Some(names[0].clone()));
let child = id.child(d.clone()).unwrap();
let mut ds = id.descriptors();
ds.push(d);
prop_assert_eq!(child, SymbolId::global(id.package().unwrap(), ds));
} else {
prop_assert!(id.child(d).is_none());
}
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn view_agrees_with_structure(id in symbol_id()) {
use sealmap_model::{DescriptorKind, IdView};
match id.view() {
IdView::Global(g) => {
let p = id.package().unwrap();
prop_assert_eq!(g.manager, p.manager());
prop_assert_eq!(&*g.package, p.name());
match p.version() {
Version::Current => prop_assert!(g.version.is_none()),
Version::Release(v) => prop_assert_eq!(g.version.as_deref(), Some(v.as_str())),
}
let ds = id.descriptors();
prop_assert_eq!(g.descriptors.len(), ds.len());
for (v, d) in g.descriptors.iter().zip(&ds) {
prop_assert_eq!(&*v.name, d.name());
let (kind, dis) = match d.suffix() {
Suffix::Namespace => (DescriptorKind::Namespace, None),
Suffix::Type => (DescriptorKind::Type, None),
Suffix::Term => (DescriptorKind::Term, None),
Suffix::Method { disambiguator } => (DescriptorKind::Method, disambiguator.as_deref()),
Suffix::TypeParameter => (DescriptorKind::TypeParameter, None),
Suffix::Parameter => (DescriptorKind::Parameter, None),
Suffix::Meta => (DescriptorKind::Meta, None),
Suffix::Macro => (DescriptorKind::Macro, None),
};
prop_assert_eq!(v.kind, kind);
prop_assert_eq!(v.disambiguator, dis);
}
}
IdView::Path(segs) => {
let want = id.segments().unwrap();
prop_assert_eq!(segs.iter().map(|s| s.to_string()).collect::<Vec<_>>(), want);
}
IdView::Unresolved(n) => {
prop_assert!(id.is_unresolved());
prop_assert_eq!(n, id.name());
}
}
}
}