use super::*;
use std::path::PathBuf;
use std::str::FromStr;
mod date;
mod fingerprint;
mod identity;
mod source_tree_path;
#[test]
fn glob_question_mark_matches_one_unicode_character() {
assert!(glob_matches_str("docs/?.md", "docs/é.md"));
assert!(glob_matches_str("docs/??.md", "docs/éx.md"));
assert!(!glob_matches_str("docs/?.md", "docs/ee.md"));
}
#[test]
fn finding_kind_accepts_hyphenated_cli_aliases() {
assert_eq!(
FindingKind::from_str("non-rust"),
Ok(FindingKind::NonRustFile)
);
assert_eq!(
FindingKind::from_str("lint-exception"),
Ok(FindingKind::LintException)
);
assert_eq!(
FindingKind::from_str("generated-code"),
Ok(FindingKind::GeneratedCode)
);
}
#[test]
fn source_code_kinds_require_selector_identity() {
assert!(FindingKind::Panic.requires_source_selector_identity());
assert!(FindingKind::Unsafe.requires_source_selector_identity());
assert!(FindingKind::LintException.requires_source_selector_identity());
assert!(!FindingKind::NonRustFile.requires_source_selector_identity());
assert!(!FindingKind::GeneratedCode.requires_source_selector_identity());
assert!(!FindingKind::PolicyException.requires_source_selector_identity());
}
#[test]
fn json_escape_covers_quotes_backslashes_whitespace_and_control_chars() {
assert_eq!(
json_escape("quote: \" slash: \\ newline:\n tab:\t return:\r bell:\u{0007}"),
"quote: \\\" slash: \\\\ newline:\\n tab:\\t return:\\r bell:\\u0007"
);
}
#[test]
fn allow_config_empty_sets_document_defaults() {
let config = AllowConfig::empty();
assert_eq!(config.schema_version, "0.1");
assert_eq!(config.policy, "cargo-allow");
assert_eq!(config.owner, None);
assert_eq!(config.status.as_deref(), Some("active"));
assert_eq!(config.workspace, WorkspaceConfig::default());
assert_eq!(config.requirements, Requirements::default());
assert!(config.allow.is_empty());
}
#[test]
fn match_status_strings_and_failure_modes_cover_all_statuses() {
for status in [
MatchStatus::Matched,
MatchStatus::New,
MatchStatus::Stale,
MatchStatus::Expired,
MatchStatus::ReviewDue,
MatchStatus::Ambiguous,
MatchStatus::InvalidSelector,
MatchStatus::MissingRequiredField,
MatchStatus::EvidenceMissing,
MatchStatus::BaselineDebt,
] {
let (expected_name, strict_failure, no_new_failure) = match status {
MatchStatus::Matched => ("matched", false, false),
MatchStatus::New => ("new", true, true),
MatchStatus::Stale => ("stale", true, false),
MatchStatus::Expired => ("expired", true, true),
MatchStatus::ReviewDue => ("review_due", false, false),
MatchStatus::Ambiguous => ("ambiguous", true, true),
MatchStatus::InvalidSelector => ("invalid_selector", true, true),
MatchStatus::MissingRequiredField => ("missing_required_field", true, true),
MatchStatus::EvidenceMissing => ("evidence_missing", true, true),
MatchStatus::BaselineDebt => ("baseline_debt", true, false),
};
assert_eq!(status.as_str(), expected_name);
assert_eq!(status.is_failure_in_strict(), strict_failure, "{status:?}");
assert_eq!(status.is_failure_in_no_new(), no_new_failure, "{status:?}");
}
}
#[test]
fn finding_kind_display_and_parser_cover_policy_aliases_and_errors() {
assert_eq!(FindingKind::PolicyException.to_string(), "policy_exception");
assert_eq!(
FindingKind::from_str(" policy-exception "),
Ok(FindingKind::PolicyException)
);
assert_eq!(
FindingKind::from_str("generated"),
Ok(FindingKind::GeneratedCode)
);
let error = FindingKind::from_str("unknown-kind")
.unwrap_err()
.to_string();
assert!(error.contains("unsupported finding kind `unknown-kind`"));
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn path_segment() -> impl Strategy<Value = String> {
prop_oneof![
Just(".".to_string()),
Just("..".to_string()),
"[A-Za-z0-9_-]{1,8}".prop_map(|s| s),
]
}
fn relative_path_text() -> impl Strategy<Value = String> {
prop::collection::vec(path_segment(), 0..12).prop_map(|segments| segments.join("/"))
}
fn stable_text() -> impl Strategy<Value = String> {
"[A-Za-z0-9_:/|.-]{0,24}".prop_map(|s| s)
}
fn maybe_stable_text() -> impl Strategy<Value = Option<String>> {
prop::option::of(stable_text())
}
prop_compose! {
fn structural_identity_strategy()(
language in stable_text(),
ast_kind in stable_text(),
crate_name in maybe_stable_text(),
module in maybe_stable_text(),
container in maybe_stable_text(),
symbol in maybe_stable_text(),
callee in maybe_stable_text(),
macro_name in maybe_stable_text(),
lint in maybe_stable_text(),
receiver_fingerprint in maybe_stable_text(),
target_fingerprint in maybe_stable_text(),
normalized_snippet_hash in maybe_stable_text(),
line_hint in prop::option::of(any::<u32>()),
column_hint in prop::option::of(any::<u32>()),
) -> StructuralIdentity {
StructuralIdentity {
language,
crate_name,
module,
container,
ast_kind,
symbol,
callee,
macro_name,
lint,
receiver_fingerprint,
target_fingerprint,
normalized_snippet_hash,
line_hint,
column_hint,
}
}
}
proptest! {
#[test]
fn normalize_path_is_idempotent(path in relative_path_text()) {
let normalized = normalize_path(&path);
prop_assert_eq!(normalize_path(&normalized), normalized);
}
#[test]
fn normalize_path_removes_current_and_empty_segments(path in relative_path_text()) {
let normalized = normalize_path(&path);
prop_assert!(
normalized.is_empty()
|| !normalized
.split('/')
.any(|part| part.is_empty() || part == ".")
);
}
#[test]
fn double_star_glob_matches_any_number_of_whole_segments(
prefix in "[A-Za-z0-9_-]{1,8}",
middle in prop::collection::vec("[A-Za-z0-9_-]{1,8}", 0..6),
file in r"[A-Za-z0-9_-]{1,8}\.rs",
) {
let pattern = format!("{prefix}/**/*.rs");
let path = std::iter::once(prefix)
.chain(middle)
.chain(std::iter::once(file))
.collect::<Vec<_>>()
.join("/");
prop_assert!(glob_matches_str(&pattern, &path));
}
#[test]
fn subtree_ignore_pattern_does_not_match_shared_prefix_sibling(
prefix in "[A-Za-z][A-Za-z0-9_-]{0,8}",
suffix in "[A-Za-z0-9_-]{1,8}",
leaf in "[A-Za-z0-9_-]{1,8}",
) {
let pattern = format!("{prefix}/**");
let ignored = format!("{prefix}/{leaf}");
let sibling = format!("{prefix}{suffix}/{leaf}");
let patterns = vec![pattern];
prop_assert!(source_tree_path_is_ignored(&ignored, &patterns));
prop_assert!(!source_tree_path_is_ignored(&sibling, &patterns));
}
#[test]
fn date_epoch_day_roundtrips(days in 0_i64..2_000_000_i64) {
let date = SimpleDate::from_days_since_unix_epoch(days);
prop_assert_eq!(date.days_since_unix_epoch(), days);
prop_assert_eq!(SimpleDate::parse(&date.to_string()), Some(date));
}
#[test]
fn date_add_days_matches_days_until(
start_days in -2_000_000_i64..2_000_000_i64,
delta in -20_000_i64..20_000_i64,
) {
let start = SimpleDate::from_days_since_unix_epoch(start_days);
let end = start.add_days(delta);
prop_assert_eq!(start.days_until(end), delta);
prop_assert_eq!(end.days_since_unix_epoch(), start_days + delta);
}
#[test]
fn stable_hash_hex_has_expected_shape_and_is_deterministic(input in ".{0,256}") {
let first = stable_hash_hex(&input);
let second = stable_hash_hex(&input);
prop_assert_eq!(&first, &second);
prop_assert!(first.starts_with("fnv1a64:"));
prop_assert_eq!(first.len(), "fnv1a64:".len() + 16);
prop_assert!(first["fnv1a64:".len()..].chars().all(|ch| ch.is_ascii_hexdigit()));
}
#[test]
fn stable_identity_keys_ignore_location_hints(
mut identity in structural_identity_strategy(),
line_hint in any::<u32>(),
column_hint in any::<u32>(),
) {
let base = identity.stable_key();
identity.line_hint = Some(line_hint);
identity.column_hint = Some(column_hint);
prop_assert_eq!(identity.stable_key(), base);
}
#[test]
fn finding_identity_keys_ignore_span(
mut finding_path in relative_path_text(),
mut identity in structural_identity_strategy(),
family in maybe_stable_text(),
line in any::<u32>(),
column in any::<u32>(),
) {
if finding_path.is_empty() {
finding_path = "src/lib.rs".to_string();
}
identity.line_hint = Some(line);
identity.column_hint = Some(column);
let finding = Finding {
kind: FindingKind::Unsafe,
family,
path: PathBuf::from(finding_path),
span: Some(Span { line, column }),
identity,
message: "generated finding".to_string(),
};
let mut moved = finding.clone();
moved.span = Some(Span {
line: line.wrapping_add(1),
column: column.wrapping_add(1),
});
prop_assert_eq!(finding_identity_key(&finding), finding_identity_key(&moved));
}
}
}