use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use oxiland::terms::Literal;
use oxiland::utility::vocab::{dc, owl, rdf, rdfs, xsd};
use oxiland::utility::{
DigestAlgorithm, Namespace, digest_hex, digest_path, file_uri_to_path, join_iri, normalize_nfc,
normalize_nfkc, path_to_file_uri, relativize_iri, resolve_iri,
};
use oxiland::{Error, LogFacility, LogLevel, LogRecord, Model, StatementPattern, World};
#[test]
fn digest_hex_known_vectors() {
assert_eq!(
digest_hex(DigestAlgorithm::Md5, b""),
"d41d8cd98f00b204e9800998ecf8427e"
);
assert_eq!(
digest_hex(DigestAlgorithm::Sha1, b"abc"),
"a9993e364706816aba3e25717850c26c9cd0d89d"
);
assert_eq!(
digest_hex(DigestAlgorithm::Sha256, b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn digest_from_name_and_unsupported() {
assert_eq!(
DigestAlgorithm::from_name("SHA-256").unwrap(),
DigestAlgorithm::Sha256
);
let err = DigestAlgorithm::from_name("blake3").unwrap_err();
assert!(matches!(err, Error::Unsupported(_)));
}
#[test]
fn digest_path_reads_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.txt");
std::fs::write(&path, b"abc").unwrap();
let bytes = digest_path(DigestAlgorithm::Sha1, &path).unwrap();
assert_eq!(
digest_hex(DigestAlgorithm::Sha1, b"abc"),
bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()
);
}
#[test]
fn uri_join_and_relativize() {
let joined = join_iri("https://example.com/base/dir/", "leaf").unwrap();
assert_eq!(joined.as_str(), "https://example.com/base/dir/leaf");
let abs = join_iri("https://example.com/base/", "https://other.example/x").unwrap();
assert_eq!(abs.as_str(), "https://other.example/x");
let authority = join_iri("https://example.com", "leaf").unwrap();
assert_eq!(authority.as_str(), "https://example.com/leaf");
let hash_base = join_iri("https://example.com/vocab#", "Term").unwrap();
assert_eq!(hash_base.as_str(), "https://example.com/Term");
let rel = relativize_iri(
"https://example.com/base/dir/",
"https://example.com/base/dir/leaf",
)
.unwrap();
assert_eq!(rel.as_deref(), Some("leaf"));
let rel_auth = relativize_iri("https://example.com", "https://example.com/leaf").unwrap();
assert_eq!(rel_auth.as_deref(), Some("leaf"));
assert!(resolve_iri("not a uri").is_err());
}
#[test]
fn file_uri_strips_query_and_fragment() {
let path = file_uri_to_path("file:///tmp/oxiland-x?y=1#frag").unwrap();
assert_eq!(path, PathBuf::from("/tmp/oxiland-x"));
}
#[test]
fn namespace_requires_delimiter_base() {
let err = Namespace::new("ex", "https://example.com/vocab").unwrap_err();
assert!(matches!(err, Error::InvalidRdf(_)));
let ns = Namespace::new("ex", "https://example.com/vocab/").unwrap();
assert_eq!(
ns.expand("Term").unwrap().as_str(),
"https://example.com/vocab/Term"
);
}
#[test]
fn world_clones_share_log_level() {
let world = World::new();
world.set_log_level(LogLevel::Error);
let clone = world.clone();
assert_eq!(clone.log_level(), LogLevel::Error);
clone.set_log_level(LogLevel::Debug);
assert_eq!(world.log_level(), LogLevel::Debug);
drop(clone);
}
#[test]
fn file_uri_round_trip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data with space.ttl");
std::fs::write(&path, b"x").unwrap();
let uri = path_to_file_uri(&path).unwrap();
assert!(uri.as_str().starts_with("file://"));
let back = file_uri_to_path(uri.as_str()).unwrap();
assert_eq!(back.canonicalize().unwrap(), path.canonicalize().unwrap());
let err = file_uri_to_path("https://example.com/x").unwrap_err();
assert!(matches!(err, Error::InvalidRdf(_)));
#[cfg(windows)]
{
let unc = file_uri_to_path("file://remote.host/tmp/x").unwrap();
assert_eq!(unc, std::path::PathBuf::from(r"\\remote.host\tmp\x"));
}
#[cfg(not(windows))]
{
let bad = file_uri_to_path("file://remote.host/tmp/x").unwrap_err();
assert!(matches!(bad, Error::Unsupported(_)));
}
}
#[test]
fn unicode_normalization_helpers() {
let composed = normalize_nfc("e\u{0301}");
assert_eq!(composed, "\u{00e9}");
let compatibility = normalize_nfkc("fi");
assert_eq!(compatibility, "fi");
}
#[test]
fn namespace_expand_and_vocab_constants() {
let ns = Namespace::new("ex", "https://example.com/").unwrap();
assert_eq!(
ns.expand("alice").unwrap().as_str(),
"https://example.com/alice"
);
assert_eq!(
ns.expand("ex:bob").unwrap().as_str(),
"https://example.com/bob"
);
assert!(Namespace::new("", "https://example.com/").is_err());
assert_eq!(
rdf::type_().as_str(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
);
assert_eq!(
rdfs::label().as_str(),
"http://www.w3.org/2000/01/rdf-schema#label"
);
assert_eq!(
xsd::integer().as_str(),
"http://www.w3.org/2001/XMLSchema#integer"
);
assert_eq!(
owl::same_as().as_str(),
"http://www.w3.org/2002/07/owl#sameAs"
);
assert_eq!(dc::title().as_str(), "http://purl.org/dc/terms/title");
}
#[test]
fn logging_filters_and_preserves_order() {
let world = World::new();
let seen: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&seen);
world.set_log_level(LogLevel::Info);
world.set_log_handler(move |record: &LogRecord| {
sink.lock()
.unwrap()
.push(format!("{}:{}", record.level.name(), record.message));
});
world.log(LogLevel::Debug, LogFacility::Utility, "skip");
world.log(LogLevel::Info, LogFacility::Utility, "one");
world.log(LogLevel::Warn, LogFacility::Model, "two");
world.log(LogLevel::Error, LogFacility::Io, "three");
let messages = seen.lock().unwrap().clone();
assert_eq!(messages, vec!["info:one", "warn:two", "error:three"]);
}
#[test]
fn world_clones_share_log_handler() {
let world = World::new();
let seen: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
let sink = Arc::clone(&seen);
world.set_log_level(LogLevel::Debug);
world.set_log_handler(move |_record: &LogRecord| {
*sink.lock().unwrap() += 1;
});
let clone = world.clone();
drop(world);
clone.log(LogLevel::Info, LogFacility::General, "shared");
assert_eq!(*seen.lock().unwrap(), 1);
}
#[test]
fn find_early_stop_evidence_matrix() {
let model = Model::new().unwrap();
for i in 0..20 {
model
.add(oxiland::terms::Triple::new(
oxiland::terms::named_node(format!("https://example.com/s{i}")).unwrap(),
rdf::type_(),
Literal::new_simple_literal(format!("v{i}")),
))
.unwrap();
}
let mut count = 0;
for item in model.find(StatementPattern::default()) {
let _quad = item.unwrap();
count += 1;
if count == 3 {
break;
}
}
assert_eq!(count, 3);
}
#[test]
fn hash_list_std_replacements_documented() {
let mut map = HashMap::new();
map.insert("k", 1);
let list: Vec<i32> = vec![1, 2, 3];
assert_eq!(map.get("k"), Some(&1));
assert_eq!(list.len(), 3);
}
#[test]
fn malformed_path_digest_is_io_error() {
let missing = PathBuf::from("/definitely/missing/oxiland-digest-test");
let err = digest_path(DigestAlgorithm::Md5, &missing).unwrap_err();
assert!(matches!(err, Error::Io(_)));
}