use super::diagnostics::{path_hash, short_hash};
use super::paths::{FailingPathNormalizer, FsPathNormalizer, normalized_path_key};
use anyhow::{Context, Result, ensure};
use proptest::prelude::*;
use rstest::rstest;
use std::path::Path;
use tempfile::tempdir;
fn hash_input() -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(any::<u8>(), 0..256)
}
fn path_string() -> impl Strategy<Value = String> {
"[A-Za-z0-9._/-]{0,64}"
}
fn ensure_bounded_hash(hash: &str) -> Result<()> {
ensure!(
hash.len() == 16,
"hash should always be 16 characters, got {}: {hash}",
hash.len()
);
ensure!(
hash.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
"hash should be lowercase hex: {hash}"
);
Ok(())
}
proptest! {
#[test]
fn short_hash_is_always_bounded_and_hex(value in hash_input()) {
let hash = short_hash(&value);
prop_assert!(ensure_bounded_hash(&hash).is_ok(), "unbounded hash: {hash}");
}
#[test]
fn short_hash_is_deterministic(value in hash_input()) {
prop_assert_eq!(short_hash(&value), short_hash(&value));
}
#[test]
fn short_hash_does_not_echo_input(value in "[A-Za-z0-9._/-]{8,64}") {
let hash = short_hash(value.as_bytes());
prop_assert!(
!hash.contains(&value),
"hash {hash} should not contain input {value}"
);
}
#[test]
fn path_hash_is_always_bounded(value in path_string()) {
let hash = path_hash(Path::new(&value));
prop_assert!(ensure_bounded_hash(&hash).is_ok(), "unbounded hash: {hash}");
}
#[test]
fn normalized_path_key_reports_absent_paths(value in path_string()) {
let absent = format!("/nonexistent-netsuke-proptest/{value}");
prop_assert!(normalized_path_key(&FsPathNormalizer, &absent).is_err());
}
#[test]
fn normalized_path_key_is_idempotent(
value in "[A-Za-z0-9][A-Za-z0-9._-]{0,63}"
) {
let temp = tempdir().expect("create temp dir for resolvable path");
let path = temp.path().join(value);
test_support::fs::create_dir(&path).expect("create generated directory");
let once = normalized_path_key(&FsPathNormalizer, &path.to_string_lossy())
.expect("generated path must normalize");
let twice = normalized_path_key(&FsPathNormalizer, &once.to_string_lossy())
.expect("an already-resolved path must normalize again");
prop_assert_eq!(once, twice);
}
}
#[rstest]
#[case::dot_component("existing", ".")]
#[case::parent_component("existing", "../existing")]
fn normalized_path_key_resolves_non_canonical_forms(
#[case] dir_name: &str,
#[case] suffix: &str,
) -> Result<()> {
let temp = tempdir().context("create temp dir")?;
let target = temp.path().join(dir_name);
test_support::fs::create_dir(&target).context("create target dir")?;
let non_canonical = target.join(suffix);
let normalized = normalized_path_key(&FsPathNormalizer, &non_canonical.to_string_lossy())
.context("normalize non-canonical path")?;
let expected = normalized_path_key(&FsPathNormalizer, &target.to_string_lossy())
.context("normalize target path")?;
ensure!(
normalized == expected,
"non-canonical {non_canonical:?} should normalize to {expected:?}, got {normalized:?}"
);
Ok(())
}
#[test]
fn normalized_path_key_propagates_normalizer_failure() -> Result<()> {
let error = normalized_path_key(&FailingPathNormalizer, "/any/path")
.expect_err("the failing normalizer must surface its error");
ensure!(
error.kind() == std::io::ErrorKind::PermissionDenied,
"expected the normalizer's own error kind, got {error:?}"
);
Ok(())
}