use std::rc::Rc;
use proptest::prelude::*;
use sui_eval::value::{NixAttrs, Value};
use sui_spec::{hash, lock_file, narinfo, realisation};
proptest! {
#[test]
fn hash_roundtrip_through_each_encoding(
bytes in prop::collection::vec(any::<u8>(), 1..32),
) {
for encoding in ["base16", "sri"] {
let encoded = hash::encode_hash("sha256", encoding, &bytes).unwrap();
let (_, decoded) = hash::decode_hash(&encoded).unwrap();
prop_assert_eq!(decoded.clone(), bytes.clone());
}
}
}
fn narinfo_fmt() -> narinfo::NarinfoFormat {
narinfo::load_canonical().unwrap().into_iter()
.find(|f| f.name == "cppnix-narinfo-v1").unwrap()
}
proptest! {
#[test]
fn narinfo_roundtrip(
path_suffix in "[a-z]{4,12}",
nar_size in 1u64..1_000_000,
ref_count in 0usize..5,
) {
let mut references = Vec::new();
for i in 0..ref_count {
references.push(format!("/nix/store/dep{i}-x"));
}
let original = narinfo::ParsedNarInfo {
store_path: format!("/nix/store/abc-{path_suffix}"),
url: format!("nar/{path_suffix}.nar.xz"),
compression: "xz".into(),
file_hash: None,
file_size: None,
nar_hash: "sha256:xyz".into(),
nar_size,
references,
deriver: None,
system: None,
signatures: vec![],
ca: None,
};
let emitted = narinfo::emit(&original);
let reparsed = narinfo::parse(&emitted, &narinfo_fmt()).unwrap();
prop_assert_eq!(original, reparsed);
}
}
fn lock_fmt() -> lock_file::LockFileFormat {
lock_file::load_canonical().unwrap().into_iter()
.find(|f| f.name == "cppnix-flake-lock-v7").unwrap()
}
proptest! {
#[test]
fn lockfile_garbage_always_errors(garbage in "[a-zA-Z !@#$_-]{1,80}") {
if serde_json::from_str::<serde_json::Value>(&garbage).is_ok() {
return Ok(());
}
let res = lock_file::parse(&garbage, &lock_fmt());
prop_assert!(res.is_err());
}
#[test]
fn lockfile_wrong_version_always_errors(v in 0u32..50) {
prop_assume!(v != 7);
let text = format!(r#"{{ "version": {v}, "root": "x", "nodes": {{}} }}"#);
let res = lock_file::parse(&text, &lock_fmt());
prop_assert!(res.is_err());
}
}
proptest! {
#[test]
fn realisation_garbage_always_errors(garbage in "[a-z !@#]{1,40}") {
if serde_json::from_str::<serde_json::Value>(&garbage).is_ok() {
return Ok(());
}
let fmt = realisation::load_canonical().unwrap().into_iter()
.find(|f| f.name == "cppnix-realisation-v1").unwrap();
let res = realisation::parse(&garbage, &fmt);
prop_assert!(res.is_err());
}
}
proptest! {
#[test]
fn hash_decode_error_for_invalid_input(input in "[xyz]{1,20}") {
prop_assume!(input.len() % 2 != 0); let res = hash::decode_hash(&input);
if let Err(sui_spec::SpecError::Interp { phase, .. }) = res {
prop_assert_eq!(phase.as_str(), "hash-decode");
}
}
}
fn attrs(pairs: Vec<(&str, Value)>) -> Rc<NixAttrs> {
let mut a = NixAttrs::new();
for (k, v) in pairs {
a.insert(k.to_string(), v);
}
Rc::new(a)
}
#[test]
fn lockfile_parse_handles_minimal_valid_input() {
let text = r#"{ "version": 7, "root": "root", "nodes": { "root": {} } }"#;
let parsed = lock_file::parse(text, &lock_fmt()).unwrap();
assert_eq!(parsed.version, 7);
assert_eq!(parsed.root, "root");
}
#[test]
fn narinfo_minimal_round_trip() {
let text = "\
StorePath: /nix/store/x-y
URL: nar/x.nar
Compression: xz
NarHash: sha256:z
NarSize: 1
";
let fmt = narinfo_fmt();
let parsed = narinfo::parse(text, &fmt).unwrap();
assert_eq!(parsed.store_path, "/nix/store/x-y");
let emitted = narinfo::emit(&parsed);
let reparsed = narinfo::parse(&emitted, &fmt).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn registry_resolve_walks_precedence_chain() {
use sui_spec::registry::{self, RegistryEntry, RegistryScope};
let registries = vec![
(RegistryScope::Global, vec![RegistryEntry {
from: "nixpkgs".into(),
to: "github:NixOS/nixpkgs/global".into(),
exact: false,
}]),
(RegistryScope::FlakeLocal, vec![RegistryEntry {
from: "nixpkgs".into(),
to: "github:NixOS/nixpkgs/local".into(),
exact: false,
}]),
];
let resolved = registry::resolve(®istries, "nixpkgs").unwrap();
assert_eq!(resolved.to, "github:NixOS/nixpkgs/local");
}
#[allow(dead_code)]
fn _shut_up() { let _ = attrs(vec![]); }