use crate::eval::eval;
use crate::value::{NixString, SmolStr, StringContext, Value};
use super::{evaluate_flake, FLAKE_EVAL_DEPTH, MAX_FLAKE_EVAL_DEPTH};
fn ev(input: &str) -> Value {
eval(input).unwrap()
}
fn drv_field(attrs: &crate::value::NixAttrs, key: &str) -> String {
crate::eval::force_value(attrs.get(key).unwrap_or_else(|| panic!("missing field {key}")))
.unwrap()
.as_string()
.unwrap()
.to_string()
}
#[test]
fn builtins_gen_list_generates_correct_list() {
let v = ev("builtins.genList (x: x * 2) 4");
assert_eq!(
v,
Value::list(vec![
Value::Int(0),
Value::Int(2),
Value::Int(4),
Value::Int(6),
]),
);
}
#[test]
fn builtins_gen_list_zero_length() {
let v = ev("builtins.genList (x: x) 0");
assert_eq!(v, Value::list(vec![]));
}
#[test]
fn builtins_elem_finds_element() {
assert_eq!(ev("builtins.elem 2 [1 2 3]"), Value::Bool(true));
}
#[test]
fn builtins_elem_missing_element() {
assert_eq!(ev("builtins.elem 5 [1 2 3]"), Value::Bool(false));
}
#[test]
fn builtins_throw_produces_error() {
let result = eval(r#"builtins.throw "kaboom""#);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("kaboom"));
}
#[test]
fn builtins_seq_forces_first_arg() {
assert_eq!(ev("builtins.seq 1 42"), Value::Int(42));
assert_eq!(ev(r#"builtins.seq "forced" true"#), Value::Bool(true));
}
#[test]
fn builtins_current_system_valid_string() {
let v = ev("builtins.currentSystem");
if let Value::String(ns) = v {
let s = &ns.chars;
assert!(
["aarch64-darwin", "x86_64-darwin", "aarch64-linux", "x86_64-linux"]
.contains(&s.as_str()),
"unexpected system string: {s}",
);
} else {
panic!("expected string for currentSystem");
}
}
#[test]
fn builtins_lang_version_is_int() {
let v = ev("builtins.langVersion");
assert!(matches!(v, Value::Int(_)));
}
#[test]
fn builtins_nix_version_is_string() {
let v = ev("builtins.nixVersion");
assert!(matches!(v, Value::String(_)));
}
#[test]
fn builtins_is_function() {
assert_eq!(ev("builtins.isFunction (x: x)"), Value::Bool(true));
assert_eq!(ev("builtins.isFunction builtins.head"), Value::Bool(true));
assert_eq!(ev("builtins.isFunction 42"), Value::Bool(false));
}
#[test]
fn builtins_is_path() {
assert_eq!(ev("builtins.isPath ./foo"), Value::Bool(true));
assert_eq!(ev("builtins.isPath 42"), Value::Bool(false));
}
#[test]
fn builtins_elem_at() {
assert_eq!(ev("builtins.elemAt [10 20 30] 1"), Value::Int(20));
}
#[test]
fn builtins_has_attr() {
assert_eq!(ev(r#"builtins.hasAttr "a" { a = 1; }"#), Value::Bool(true));
assert_eq!(ev(r#"builtins.hasAttr "b" { a = 1; }"#), Value::Bool(false));
}
#[test]
fn builtins_get_attr() {
assert_eq!(ev(r#"builtins.getAttr "a" { a = 42; }"#), Value::Int(42));
}
#[test]
fn builtins_map() {
assert_eq!(
ev("builtins.map (x: x * 2) [1 2 3]"),
Value::list(vec![Value::Int(2), Value::Int(4), Value::Int(6)]),
);
}
#[test]
fn builtins_map_empty() {
assert_eq!(ev("builtins.map (x: x) []"), Value::list(vec![]));
}
#[test]
fn builtins_filter() {
assert_eq!(
ev("builtins.filter (x: x > 2) [1 2 3 4 5]"),
Value::list(vec![Value::Int(3), Value::Int(4), Value::Int(5)]),
);
}
#[test]
fn builtins_filter_empty() {
assert_eq!(ev("builtins.filter (x: false) [1 2 3]"), Value::list(vec![]));
}
#[test]
fn builtins_foldl() {
assert_eq!(ev("builtins.foldl' (a: b: a + b) 0 [1 2 3 4]"), Value::Int(10));
}
#[test]
fn builtins_foldl_empty() {
assert_eq!(ev("builtins.foldl' (a: b: a + b) 0 []"), Value::Int(0));
}
#[test]
fn builtins_concat_map() {
assert_eq!(
ev("builtins.concatMap (x: [x (x * 2)]) [1 2 3]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(2), Value::Int(4), Value::Int(3), Value::Int(6)]),
);
}
#[test]
fn builtins_concat_lists() {
assert_eq!(
ev("builtins.concatLists [[1 2] [3] [4 5]]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4), Value::Int(5)]),
);
}
#[test]
fn builtins_all() {
assert_eq!(ev("builtins.all (x: x > 0) [1 2 3]"), Value::Bool(true));
assert_eq!(ev("builtins.all (x: x > 2) [1 2 3]"), Value::Bool(false));
}
#[test]
fn builtins_any() {
assert_eq!(ev("builtins.any (x: x > 2) [1 2 3]"), Value::Bool(true));
assert_eq!(ev("builtins.any (x: x > 5) [1 2 3]"), Value::Bool(false));
}
#[test]
fn builtins_map_attrs() {
let v = ev(r#"builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(2)));
assert_eq!(a.get("b"), Some(&Value::Int(4)));
} else { panic!("expected attrs"); }
}
#[test]
fn builtins_list_to_attrs() {
let v = ev(r#"builtins.listToAttrs [{ name = "a"; value = 1; } { name = "b"; value = 2; }]"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(1)));
assert_eq!(a.get("b"), Some(&Value::Int(2)));
} else { panic!("expected attrs"); }
}
#[test]
fn builtins_remove_attrs() {
let v = ev(r#"builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b" "c"]"#);
if let Value::Attrs(a) = v {
assert_eq!(a.len(), 1);
assert_eq!(a.get("a"), Some(&Value::Int(1)));
} else { panic!("expected attrs"); }
}
#[test]
fn builtins_concat_strings_sep() {
assert_eq!(
ev(r#"builtins.concatStringsSep ", " ["a" "b" "c"]"#),
Value::string("a, b, c"),
);
}
#[test]
fn builtins_has_prefix() {
assert_eq!(ev(r#"builtins.hasPrefix "foo" "foobar""#), Value::Bool(true));
assert_eq!(ev(r#"builtins.hasPrefix "bar" "foobar""#), Value::Bool(false));
}
#[test]
fn builtins_has_suffix() {
assert_eq!(ev(r#"builtins.hasSuffix "bar" "foobar""#), Value::Bool(true));
assert_eq!(ev(r#"builtins.hasSuffix "foo" "foobar""#), Value::Bool(false));
}
#[test]
fn builtins_replace_strings() {
assert_eq!(
ev(r#"builtins.replaceStrings ["foo" "bar"] ["FOO" "BAR"] "foobar""#),
Value::string("FOOBAR"),
);
}
#[test]
fn builtins_ceil_floor() {
assert_eq!(ev("builtins.ceil 3.2"), Value::Int(4));
assert_eq!(ev("builtins.floor 3.8"), Value::Int(3));
}
#[test]
fn builtins_try_eval() {
let v = ev("builtins.tryEval 42");
if let Value::Attrs(a) = v {
assert_eq!(a.get("success"), Some(&Value::Bool(true)));
assert_eq!(a.get("value"), Some(&Value::Int(42)));
} else { panic!("expected attrs"); }
}
#[test]
fn builtins_trace() {
assert_eq!(ev(r#"builtins.trace "debug msg" 42"#), Value::Int(42));
}
#[test]
fn builtins_function_args() {
let v = ev("builtins.functionArgs ({ a, b ? 1 }: a)");
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Bool(false)));
assert_eq!(a.get("b"), Some(&Value::Bool(true)));
} else { panic!("expected attrs"); }
}
#[test]
fn builtins_sort() {
assert_eq!(
ev("builtins.sort (a: b: a < b) [3 1 2]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn builtins_sort_large_list() {
let expr = "builtins.sort (a: b: a < b) (builtins.genList (i: 1000 - i) 1000)";
let result = ev(expr);
let expected: Vec<Value> = (1..=1000).map(Value::Int).collect();
assert_eq!(result, Value::list(expected));
}
#[test]
fn builtins_sort_already_sorted() {
let expr = "builtins.sort (a: b: a < b) (builtins.genList (i: i) 100)";
let result = ev(expr);
let expected: Vec<Value> = (0..100).map(Value::Int).collect();
assert_eq!(result, Value::list(expected));
}
#[test]
fn builtins_sort_empty() {
assert_eq!(
ev("builtins.sort (a: b: a < b) []"),
Value::list(vec![]),
);
}
#[test]
fn builtins_sort_single_element() {
assert_eq!(
ev("builtins.sort (a: b: a < b) [42]"),
Value::list(vec![Value::Int(42)]),
);
}
#[test]
fn builtins_map_large_list() {
let expr = "builtins.map (x: x * 2) (builtins.genList (i: i) 1000)";
let result = ev(expr);
let expected: Vec<Value> = (0..1000).map(|i| Value::Int(i * 2)).collect();
assert_eq!(result, Value::list(expected));
}
#[test]
fn builtins_cat_attrs() {
assert_eq!(
ev(r#"builtins.catAttrs "a" [{ a = 1; } { b = 2; } { a = 3; }]"#),
Value::list(vec![Value::Int(1), Value::Int(3)]),
);
}
#[test]
fn builtins_concat_strings() {
assert_eq!(
ev(r#"builtins.concatStrings ["hello" " " "world"]"#),
Value::string("hello world"),
);
}
#[test]
fn builtins_concat_strings_empty() {
assert_eq!(
ev(r#"builtins.concatStrings []"#),
Value::string(""),
);
}
#[test]
fn builtins_partition_basic() {
let v = ev("builtins.partition (x: x > 2) [1 2 3 4 5]");
if let Value::Attrs(a) = v {
assert_eq!(
a.get("right"),
Some(&Value::list(vec![Value::Int(3), Value::Int(4), Value::Int(5)])),
);
assert_eq!(
a.get("wrong"),
Some(&Value::list(vec![Value::Int(1), Value::Int(2)])),
);
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_partition_all_right() {
let v = ev("builtins.partition (x: true) [1 2 3]");
if let Value::Attrs(a) = v {
assert_eq!(
a.get("right"),
Some(&Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)])),
);
assert_eq!(a.get("wrong"), Some(&Value::list(vec![])));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_partition_empty() {
let v = ev("builtins.partition (x: true) []");
if let Value::Attrs(a) = v {
assert_eq!(a.get("right"), Some(&Value::list(vec![])));
assert_eq!(a.get("wrong"), Some(&Value::list(vec![])));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_group_by_basic() {
let v = ev(r#"builtins.groupBy (x: x) ["a" "b" "a" "c" "b"]"#);
if let Value::Attrs(a) = v {
assert_eq!(
a.get("a"),
Some(&Value::list(vec![
Value::string("a"),
Value::string("a"),
])),
);
assert_eq!(
a.get("b"),
Some(&Value::list(vec![
Value::string("b"),
Value::string("b"),
])),
);
assert_eq!(
a.get("c"),
Some(&Value::list(vec![Value::string("c")])),
);
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_group_by_empty() {
let v = ev(r#"builtins.groupBy (x: x) []"#);
if let Value::Attrs(a) = v {
assert!(a.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_zip_attrs_with_basic() {
let v = ev("builtins.zipAttrsWith (name: values: values) [{ a = 1; } { a = 2; b = 3; }]");
if let Value::Attrs(a) = v {
assert_eq!(
a.get("a"),
Some(&Value::list(vec![Value::Int(1), Value::Int(2)])),
);
assert_eq!(
a.get("b"),
Some(&Value::list(vec![Value::Int(3)])),
);
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_zip_attrs_with_sum() {
let v = ev(r#"builtins.zipAttrsWith (name: values: builtins.foldl' (a: b: a + b) 0 values) [{ x = 1; } { x = 2; } { x = 3; }]"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("x"), Some(&Value::Int(6)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_compare_versions_equal() {
assert_eq!(ev(r#"builtins.compareVersions "1.2.3" "1.2.3""#), Value::Int(0));
}
#[test]
fn builtins_compare_versions_less() {
assert_eq!(ev(r#"builtins.compareVersions "1.2.3" "1.2.4""#), Value::Int(-1));
assert_eq!(ev(r#"builtins.compareVersions "1.2" "1.3""#), Value::Int(-1));
}
#[test]
fn builtins_compare_versions_greater() {
assert_eq!(ev(r#"builtins.compareVersions "1.3.0" "1.2.9""#), Value::Int(1));
}
#[test]
fn builtins_compare_versions_pre() {
assert_eq!(ev(r#"builtins.compareVersions "1.0pre1" "1.0.1""#), Value::Int(-1));
}
#[test]
fn builtins_parse_drv_name_basic() {
let v = ev(r#"builtins.parseDrvName "hello-2.10""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("name"), Some(&Value::string("hello")));
assert_eq!(a.get("version"), Some(&Value::string("2.10")));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_drv_name_no_version() {
let v = ev(r#"builtins.parseDrvName "hello""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("name"), Some(&Value::string("hello")));
assert_eq!(a.get("version"), Some(&Value::string("")));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_drv_name_complex() {
let v = ev(r#"builtins.parseDrvName "openssl-1.1.1k""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("name"), Some(&Value::string("openssl")));
assert_eq!(a.get("version"), Some(&Value::string("1.1.1k")));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_base_name_of() {
assert_eq!(
ev(r#"builtins.baseNameOf "/nix/store/abc-hello""#),
Value::string("abc-hello"),
);
assert_eq!(
ev(r#"builtins.baseNameOf "hello.txt""#),
Value::string("hello.txt"),
);
}
#[test]
fn builtins_dir_of_string() {
assert_eq!(
ev(r#"builtins.dirOf "/nix/store/abc""#),
Value::string("/nix/store"),
);
assert_eq!(
ev(r#"builtins.dirOf "/foo""#),
Value::string("/"),
);
}
#[test]
fn builtins_dir_of_path() {
assert_eq!(
ev("builtins.dirOf /nix/store/abc"),
Value::Path(Box::new(SmolStr::from("/nix/store"))),
);
}
#[test]
fn builtins_read_file() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_read_file.txt");
std::fs::write(&path, "hello from test").unwrap();
let expr = format!(r#"builtins.readFile "{}""#, path.display());
let v = eval(&expr).unwrap();
if let Value::String(ns) = v {
assert_eq!(ns.chars, "hello from test");
} else {
panic!("expected string");
}
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_read_file_missing() {
let result = eval(r#"builtins.readFile "/nonexistent/path/file.txt""#);
assert!(result.is_err());
}
#[test]
fn builtins_add_error_context_passthrough() {
assert_eq!(
ev(r#"builtins.addErrorContext "context msg" 42"#),
Value::Int(42),
);
}
#[test]
fn functor_basic() {
assert_eq!(
ev("let s = { __functor = self: x: self.value + x; value = 10; }; in s 5"),
Value::Int(15),
);
}
#[test]
fn functor_nested() {
assert_eq!(
ev("let s = { __functor = self: x: x * 2; }; in s 21"),
Value::Int(42),
);
}
#[test]
fn functor_with_update() {
assert_eq!(
ev(r#"
let
base = { __functor = self: x: self.v + x; v = 0; };
extended = base // { v = 100; };
in extended 5
"#),
Value::Int(105),
);
}
#[test]
fn to_string_protocol_interpolation() {
assert_eq!(
ev(r#"let s = { __toString = self: "hello"; }; in "${s}""#),
Value::string("hello"),
);
}
#[test]
fn to_string_protocol_with_self() {
assert_eq!(
ev(r#"let s = { __toString = self: self.name; name = "world"; }; in "${s}""#),
Value::string("world"),
);
}
#[test]
fn to_string_protocol_via_builtin() {
assert_eq!(
ev(r#"builtins.toString { __toString = self: "custom"; }"#),
Value::string("custom"),
);
}
#[test]
fn builtins_hash_string_sha256() {
let v = ev(r#"builtins.hashString "sha256" "hello""#);
if let Value::String(ns) = v {
let s = &ns.chars;
assert_eq!(s.len(), 64); assert_eq!(*s, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
} else {
panic!("expected string");
}
}
#[test]
fn builtins_hash_string_sha512() {
let v = ev(r#"builtins.hashString "sha512" "hello""#);
if let Value::String(ns) = v {
assert_eq!(ns.chars.len(), 128); } else {
panic!("expected string");
}
}
#[test]
fn builtins_match_regex() {
assert_eq!(
ev(r#"builtins.match "([0-9]+)\\.([0-9]+)" "1.23""#),
Value::list(vec![Value::string("1"), Value::string("23")]),
);
assert_eq!(
ev(r#"builtins.match "([0-9]+)" "abc""#),
Value::Null,
);
}
#[test]
fn builtins_match_full_string() {
assert_eq!(
ev(r#"builtins.match "([0-9]+)" "42""#),
Value::list(vec![Value::string("42")]),
);
assert_eq!(
ev(r#"builtins.match "([0-9]+)" "abc42def""#),
Value::Null,
);
}
#[test]
fn builtins_import_file() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_import.nix");
std::fs::write(&path, "{ x = 42; }").unwrap();
let expr = format!(r#"(builtins.import "{}").x"#, path.display());
let v = eval(&expr).unwrap();
assert_eq!(v, Value::Int(42));
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_import_expr() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_import_expr.nix");
std::fs::write(&path, "1 + 2").unwrap();
let expr = format!(r#"builtins.import "{}""#, path.display());
let v = eval(&expr).unwrap();
assert_eq!(v, Value::Int(3));
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_derivation_returns_real_paths() {
let v = eval(r#"builtins.derivation { name = "test"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
let a = match v {
Value::Attrs(a) => a,
other => panic!("expected attrs, got {other:?}"),
};
assert_eq!(a.get("type"), Some(&Value::string("derivation")));
assert_eq!(a.get("name"), Some(&Value::string("test")));
let drv_path = drv_field(&a, "drvPath");
assert!(drv_path.starts_with("/nix/store/"), "drvPath: {drv_path}");
assert!(drv_path.ends_with("-test.drv"), "drvPath: {drv_path}");
let drv_basename = drv_path.strip_prefix("/nix/store/").unwrap();
assert_eq!(drv_basename.len(), 32 + 1 + "test.drv".len());
let out_path = drv_field(&a, "outPath");
assert!(out_path.starts_with("/nix/store/"));
assert!(out_path.ends_with("-test"));
assert_ne!(drv_path, out_path);
}
#[test]
fn builtins_derivation_is_deterministic() {
let expr = r#"builtins.derivation {
name = "hello";
system = "x86_64-linux";
builder = "/bin/sh";
args = [ "-e" "build.sh" ];
}"#;
let a1 = eval(expr).unwrap().to_attrs().unwrap();
let a2 = eval(expr).unwrap().to_attrs().unwrap();
assert_eq!(
a1.get("drvPath").unwrap().to_str().unwrap(),
a2.get("drvPath").unwrap().to_str().unwrap(),
);
assert_eq!(
a1.get("outPath").unwrap().to_str().unwrap(),
a2.get("outPath").unwrap().to_str().unwrap(),
);
}
#[test]
fn builtins_derivation_different_names_produce_different_paths() {
let v1 = eval(r#"builtins.derivation { name = "foo"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
let v2 = eval(r#"builtins.derivation { name = "bar"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
let p1 = drv_field(&v1.as_attrs().unwrap(), "drvPath");
let p2 = drv_field(&v2.as_attrs().unwrap(), "drvPath");
assert_ne!(p1, p2);
}
#[test]
fn builtins_derivation_multiple_outputs() {
let v = eval(r#"builtins.derivation {
name = "multi";
system = "x86_64-linux";
builder = "/bin/sh";
outputs = [ "out" "dev" "lib" ];
}"#).unwrap();
let a = v.as_attrs().unwrap();
assert_eq!(a.get("type"), Some(&Value::string("derivation")));
for out_name in ["out", "dev", "lib"] {
let sub = a
.get(out_name)
.unwrap_or_else(|| panic!("missing output {out_name}"));
let sub_attrs = sub.as_attrs().unwrap();
assert_eq!(sub_attrs.get("type"), Some(&Value::string("derivation")));
assert_eq!(
sub_attrs.get("outputName"),
Some(&Value::string(out_name)),
);
assert!(sub_attrs.contains_key("outPath"));
assert!(sub_attrs.contains_key("drvPath"));
}
let out_p = drv_field(&a.get("out").unwrap().as_attrs().unwrap(), "outPath");
let dev_p = drv_field(&a.get("dev").unwrap().as_attrs().unwrap(), "outPath");
let lib_p = drv_field(&a.get("lib").unwrap().as_attrs().unwrap(), "outPath");
assert_ne!(out_p, dev_p);
assert_ne!(out_p, lib_p);
assert_ne!(dev_p, lib_p);
assert!(dev_p.ends_with("-multi-dev"));
assert!(lib_p.ends_with("-multi-lib"));
assert!(out_p.ends_with("-multi"));
}
#[test]
fn builtins_derivation_fixed_output() {
let v = eval(r#"builtins.derivation {
name = "src.tar.gz";
system = "x86_64-linux";
builder = "/bin/curl";
outputHash = "1b0ri5lsf45dknj8bfxi1syz35kmab77apxxg1yrf33la1qm3kc7";
outputHashAlgo = "sha256";
outputHashMode = "flat";
}"#).unwrap();
let a = v.as_attrs().unwrap();
assert_eq!(a.get("type"), Some(&Value::string("derivation")));
let out_path = drv_field(&a, "outPath");
assert!(out_path.ends_with("-src.tar.gz"));
assert!(drv_field(&a, "drvPath").ends_with("-src.tar.gz.drv"));
}
#[test]
fn builtins_derivation_fixed_output_recursive_differs_from_flat() {
let flat = eval(r#"builtins.derivation {
name = "x";
system = "x86_64-linux";
builder = "/bin/sh";
outputHash = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2";
outputHashAlgo = "sha256";
outputHashMode = "flat";
}"#).unwrap();
let rec = eval(r#"builtins.derivation {
name = "x";
system = "x86_64-linux";
builder = "/bin/sh";
outputHash = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2";
outputHashAlgo = "sha256";
outputHashMode = "recursive";
}"#).unwrap();
let p1 = drv_field(&flat.as_attrs().unwrap(), "outPath");
let p2 = drv_field(&rec.as_attrs().unwrap(), "outPath");
assert_ne!(p1, p2);
}
#[test]
fn builtins_derivation_returns_drv_and_out_path() {
let v = eval(r#"builtins.derivation { name = "x"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
let a = v.as_attrs().unwrap();
assert!(a.contains_key("drvPath"));
assert!(a.contains_key("outPath"));
}
static DRV_WRITE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn eval_drv_in_temp_store_inner(expr: &str, dir: &std::path::Path) -> Value {
unsafe { std::env::set_var("SUI_STORE_DIR", dir) };
let result = eval(expr).unwrap();
let forced = if let Ok(attrs) = result.to_attrs() {
let mut flat = crate::value::NixAttrs::new();
for (k, v) in attrs.iter() {
let fv = crate::eval::force_value(v).unwrap_or_else(|_| v.clone());
let fv = match fv {
crate::value::Value::Attrs(inner) => {
let mut inner_flat = crate::value::NixAttrs::new();
for (ik, iv) in inner.iter() {
inner_flat.insert(ik.clone(), crate::eval::force_value(iv).unwrap_or_else(|_| iv.clone()));
}
crate::value::Value::Attrs(std::rc::Rc::new(inner_flat))
}
crate::value::Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items.iter() {
let fit = crate::eval::force_value(it).unwrap_or_else(|_| it.clone());
let fit = match fit {
crate::value::Value::Attrs(inner) => {
let mut inner_flat = crate::value::NixAttrs::new();
for (ik, iv) in inner.iter() {
inner_flat.insert(ik.clone(), crate::eval::force_value(iv).unwrap_or_else(|_| iv.clone()));
}
crate::value::Value::Attrs(std::rc::Rc::new(inner_flat))
}
other => other,
};
out.push(fit);
}
crate::value::Value::List(std::rc::Rc::new(crate::value::NixList::new(out)))
}
other => other,
};
flat.insert(k.clone(), fv);
}
crate::value::Value::Attrs(std::rc::Rc::new(flat))
} else {
result
};
unsafe { std::env::remove_var("SUI_STORE_DIR") };
forced
}
fn make_drv_temp_dir(label: &str) -> std::path::PathBuf {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"sui-drv-{label}-{}-{n}",
std::process::id(),
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn drv_write_creates_file_on_disk() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("create");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation { name = "hello"; system = "x86_64-linux"; builder = "/bin/sh"; }"#,
&store_dir,
);
let a = v.as_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().as_string().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let p = std::path::Path::new(&disk_path);
assert!(p.exists(), "expected .drv file at {disk_path}");
let content = std::fs::read_to_string(p).unwrap();
assert!(content.starts_with("Derive("), "expected ATerm, got: {}", &content[..40.min(content.len())]);
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_roundtrips_through_parse() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("roundtrip");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation { name = "roundtrip"; system = "x86_64-linux"; builder = "/bin/sh"; args = ["-c" "echo hi"]; }"#,
&store_dir,
);
let a = v.to_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().to_str().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let content = std::fs::read(&disk_path).unwrap();
let parsed = sui_compat::derivation::Derivation::parse(&content).unwrap();
assert_eq!(parsed.system, "x86_64-linux");
assert_eq!(parsed.builder, "/bin/sh");
assert_eq!(parsed.args, vec!["-c", "echo hi"]);
let out = parsed.outputs.get("out").unwrap();
assert!(!out.path.is_empty(), "output path should be populated");
assert!(out.path.starts_with("/nix/store/"));
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_is_idempotent() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("idem");
unsafe { std::env::set_var("SUI_STORE_DIR", &store_dir) };
let expr = r#"builtins.derivation { name = "idem"; system = "x86_64-linux"; builder = "/bin/sh"; }"#;
let v1 = eval(expr).unwrap();
let v2 = eval(expr).unwrap();
let a1 = v1.as_attrs().unwrap();
let a2 = v2.as_attrs().unwrap();
let p1 = crate::eval::force_value(a1.get("drvPath").unwrap()).unwrap().as_string().unwrap().to_string();
let p2 = crate::eval::force_value(a2.get("drvPath").unwrap()).unwrap().as_string().unwrap().to_string();
unsafe { std::env::remove_var("SUI_STORE_DIR") };
assert_eq!(p1, p2, "same derivation must produce same drvPath");
let disk_path = p1.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
assert!(std::path::Path::new(&disk_path).exists());
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_path_matches_filename() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("pathcheck");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation { name = "pathcheck"; system = "x86_64-linux"; builder = "/bin/sh"; }"#,
&store_dir,
);
let a = v.as_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().as_string().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let returned_basename = std::path::Path::new(&*drv_path)
.file_name()
.unwrap()
.to_string_lossy();
let disk_basename = std::path::Path::new(&disk_path)
.file_name()
.unwrap()
.to_string_lossy();
assert_eq!(returned_basename, disk_basename);
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_fixed_output_creates_file() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("fod");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation {
name = "fod";
system = "x86_64-linux";
builder = "/bin/curl";
outputHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
outputHashAlgo = "sha256";
outputHashMode = "flat";
}"#,
&store_dir,
);
let a = v.as_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().as_string().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let p = std::path::Path::new(&disk_path);
assert!(p.exists(), "expected FOD .drv at {disk_path}");
let content = std::fs::read(p).unwrap();
let parsed = sui_compat::derivation::Derivation::parse(&content).unwrap();
let out = parsed.outputs.get("out").unwrap();
assert_eq!(
out.hash,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(out.hash_algo, "sha256");
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_env_contains_output_paths() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("envtest");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation { name = "envtest"; system = "x86_64-linux"; builder = "/bin/sh"; }"#,
&store_dir,
);
let a = v.as_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().as_string().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let content = std::fs::read(&disk_path).unwrap();
let parsed = sui_compat::derivation::Derivation::parse(&content).unwrap();
let out_env = parsed.env.get("out").expect("env should contain 'out'");
assert!(out_env.starts_with("/nix/store/"), "out env: {out_env}");
assert!(out_env.ends_with("-envtest"), "out env: {out_env}");
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn drv_write_multiple_outputs_all_in_env() {
let _g = DRV_WRITE_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let store_dir = make_drv_temp_dir("multi-env");
let v = eval_drv_in_temp_store_inner(
r#"builtins.derivation {
name = "multi-env";
system = "x86_64-linux";
builder = "/bin/sh";
outputs = ["out" "dev" "lib"];
}"#,
&store_dir,
);
let a = v.as_attrs().unwrap();
let drv_path = a.get("drvPath").unwrap().as_string().unwrap();
let disk_path = drv_path.replacen("/nix/store", &store_dir.to_string_lossy(), 1);
let content = std::fs::read(&disk_path).unwrap();
let parsed = sui_compat::derivation::Derivation::parse(&content).unwrap();
for output_name in ["out", "dev", "lib"] {
let env_val = parsed.env.get(output_name)
.unwrap_or_else(|| panic!("env missing '{output_name}'"));
assert!(env_val.starts_with("/nix/store/"), "{output_name} env: {env_val}");
}
for output_name in ["out", "dev", "lib"] {
let out = parsed.outputs.get(output_name)
.unwrap_or_else(|| panic!("outputs missing '{output_name}'"));
assert!(!out.path.is_empty(), "output path for '{output_name}' is empty");
}
let _ = std::fs::remove_dir_all(&store_dir);
}
#[test]
fn builtins_fetchurl_exists_as_builtin() {
let dir = std::env::temp_dir().join("sui_eval_test_fetchurl");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let payload = b"fetchurl-test-content";
let file = dir.join("payload.txt");
std::fs::write(&file, payload).unwrap();
let file_url = format!("file://{}", file.display());
let expr = format!(r#"builtins.fetchurl "{}""#, file_url);
let v = eval(&expr).unwrap();
if let Value::Path(p) = v {
let content = std::fs::read_to_string(p.as_str()).unwrap();
assert_eq!(content, "fetchurl-test-content");
} else {
panic!("expected path, got {v}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_fetchurl_attrset_form() {
let dir = std::env::temp_dir().join("sui_eval_test_fetchurl_attr");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let payload = b"attr-form-content";
let file = dir.join("payload.txt");
std::fs::write(&file, payload).unwrap();
let file_url = format!("file://{}", file.display());
let expr = format!(
r#"builtins.fetchurl {{ url = "{}"; }}"#,
file_url
);
let v = eval(&expr).unwrap();
assert!(matches!(v, Value::Path(_)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_fetchurl_bad_type_errors() {
let result = eval("builtins.fetchurl 42");
assert!(result.is_err());
}
#[test]
fn builtins_read_dir() {
let dir = std::env::temp_dir().join("sui_eval_test_readdir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("file.txt"), "content").unwrap();
std::fs::create_dir(dir.join("subdir")).unwrap();
let expr = format!(r#"builtins.readDir "{}""#, dir.display());
let v = eval(&expr).unwrap();
if let Value::Attrs(a) = v {
assert_eq!(a.get("file.txt"), Some(&Value::string("regular")));
assert_eq!(a.get("subdir"), Some(&Value::string("directory")));
} else {
panic!("expected attrs");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_read_dir_empty() {
let dir = std::env::temp_dir().join("sui_eval_test_readdir_empty");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let expr = format!(r#"builtins.readDir "{}""#, dir.display());
let v = eval(&expr).unwrap();
if let Value::Attrs(a) = v {
assert!(a.is_empty());
} else {
panic!("expected attrs");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_path_with_file() {
let dir = std::env::temp_dir().join("sui_eval_test_builtins_path");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("hello.txt");
std::fs::write(&file, "hello world").unwrap();
let expr = format!(
r#"builtins.path {{ path = "{}"; name = "test"; }}"#,
file.display()
);
let v = eval(&expr).unwrap();
let s = v.as_string().unwrap_or_else(|_| panic!("expected string, got {v}"));
assert!(s.starts_with("/nix/store/"));
assert!(s.ends_with("-test"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_path_default_name() {
let dir = std::env::temp_dir().join("sui_eval_test_builtins_path_dn");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("myfile.txt");
std::fs::write(&file, "content").unwrap();
let expr = format!(
r#"builtins.path {{ path = "{}"; }}"#,
file.display()
);
let v = eval(&expr).unwrap();
let s = v.as_string().unwrap_or_else(|_| panic!("expected string, got {v}"));
assert!(s.starts_with("/nix/store/"));
assert!(s.ends_with("-myfile.txt"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_placeholder() {
assert_eq!(
ev(r#"builtins.placeholder "out""#),
Value::string("/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"),
);
}
#[test]
fn builtins_get_flake_path_based() {
let dir = std::env::temp_dir().join("sui_eval_test_getflake");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("flake.nix"),
r#"{ description = "test flake"; outputs = { self }: { value = 42; }; }"#,
)
.unwrap();
let expr = format!(r#"(builtins.getFlake "{}").value"#, dir.display());
let v = eval(&expr).unwrap();
assert_eq!(v, Value::Int(42));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_get_flake_accepts_indirect_ref_offline() {
let result = eval(r#"builtins.getFlake "nixpkgs""#);
match result {
Err(crate::value::EvalError::NotImplemented(msg)) => {
panic!("getFlake should route indirect refs, not reject them: {msg}")
}
Ok(_) | Err(_) => {} }
}
#[test]
fn flake_minimal_no_inputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "test flake";
outputs = { self }: { packages.default = "hello"; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").packages.default"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn flake_with_self_output_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "test flake";
outputs = { self }: { result = self.outPath; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
let out = result.as_string().unwrap();
assert!(out.starts_with("/nix/store/"), "self.outPath must be a store path, got {out}");
assert!(out.ends_with("-source"), "self.outPath must end with -source, got {out}");
assert_ne!(out, flake_path, "self.outPath must NOT be the raw filesystem path");
}
#[test]
fn flake_description_accessible() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "my flake";
outputs = { self }: { packages.default = self.outPath; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").packages.default"#);
assert!(eval(&expr).is_ok());
}
#[test]
fn flake_path_prefix_supported() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
outputs = { self }: { value = "ok"; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "path:{flake_path}").value"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "ok");
}
#[test]
fn flake_with_locked_input_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { result = dep.outPath; };
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-DEPDEPDEPDEPDEPDEPDEPDEPDEPDEPDEPDEPDEPDEPD=",
"path": "/var/empty/dep",
"type": "path"
},
"original": {
"type": "path",
"url": "/var/empty/dep"
},
"flake": false
},
"root": {
"inputs": {
"dep": "dep"
}
}
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "/var/empty/dep");
}
#[test]
fn flake_missing_outputs_errors() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{ description = "no outputs"; }"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"builtins.getFlake "{flake_path}""#);
assert!(eval(&expr).is_err());
}
#[test]
fn flake_input_source_info_populated() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { result = dep.sourceInfo.narHash; };
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-XYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZ=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(
result.as_string().unwrap(),
"sha256-XYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZ="
);
}
#[test]
fn flake_input_last_modified_accessible() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { result = dep.lastModified; };
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000042,
"narHash": "sha256-AAAA=",
"path": "/tmp",
"type": "path"
},
"original": { "type": "path", "url": "/tmp" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Int(1_700_000_042));
}
#[test]
fn flake_input_rev_and_short_rev() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { r = dep.rev; s = dep.shortRev; };
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-BBB=",
"rev": "abc123def456abc123def456abc123def456abc1",
"path": "/tmp",
"type": "path"
},
"original": { "type": "path", "url": "/tmp" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let rev_expr = format!(r#"(builtins.getFlake "{flake_path}").r"#);
let short_expr = format!(r#"(builtins.getFlake "{flake_path}").s"#);
let rev = eval(&rev_expr).unwrap();
let short = eval(&short_expr).unwrap();
assert_eq!(
rev.as_string().unwrap(),
"abc123def456abc123def456abc123def456abc1"
);
assert_eq!(short.as_string().unwrap(), "abc123d");
}
#[test]
fn flake_non_flake_input_skips_recursive_eval() {
let dep_dir = tempfile::tempdir().unwrap();
std::fs::write(
dep_dir.path().join("flake.nix"),
r#"{ outputs = { self }: { should_not_exist = true; }; }"#,
)
.unwrap();
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: {
has_attr = builtins.hasAttr "should_not_exist" dep;
};
}"#,
)
.unwrap();
let dep_path = dep_dir.path().to_string_lossy().to_string();
std::fs::write(
dir.path().join("flake.lock"),
format!(
r#"{{
"nodes": {{
"dep": {{
"flake": false,
"locked": {{
"lastModified": 1700000000,
"narHash": "sha256-NOFLAKEDEP=",
"path": "{dep_path}",
"type": "path"
}},
"original": {{ "type": "path", "url": "{dep_path}" }}
}},
"root": {{ "inputs": {{ "dep": "dep" }} }}
}},
"root": "root",
"version": 7
}}"#
),
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").has_attr"#);
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Bool(false));
}
#[test]
fn flake_recursive_flake_input_merges_outputs() {
let dep_dir = tempfile::tempdir().unwrap();
std::fs::write(
dep_dir.path().join("flake.nix"),
r#"{
description = "dependency flake";
outputs = { self }: { lib.greet = "hello from dep"; };
}"#,
)
.unwrap();
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { result = dep.lib.greet; };
}"#,
)
.unwrap();
let dep_path = dep_dir.path().to_string_lossy().to_string();
std::fs::write(
dir.path().join("flake.lock"),
format!(
r#"{{
"nodes": {{
"dep": {{
"locked": {{
"lastModified": 1700000000,
"narHash": "sha256-FLAKEDEP=",
"path": "{dep_path}",
"type": "path"
}},
"original": {{ "type": "path", "url": "{dep_path}" }}
}},
"root": {{ "inputs": {{ "dep": "dep" }} }}
}},
"root": "root",
"version": 7
}}"#
),
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "hello from dep");
}
#[test]
fn flake_getflake_github_prefix_invalid_ref_errors() {
let result = eval(r#"builtins.getFlake "github:justowner""#);
assert!(result.is_err());
}
#[test]
fn flake_input_source_info_outpath_matches() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: {
result = dep.outPath == dep.sourceInfo.outPath;
};
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-MATCH=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Bool(true));
}
#[test]
fn flake_self_description_accessible_in_outputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "my awesome flake";
outputs = { self }: { desc = self.description; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").desc"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "my awesome flake");
}
#[test]
fn flake_multiple_inputs_all_accessible() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.a = { };
inputs.b = { };
outputs = { self, a, b }: {
result = "${a.narHash}:${b.narHash}";
};
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"a": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-AAAA=",
"path": "/tmp/a",
"type": "path"
},
"original": { "type": "path", "url": "/tmp/a" },
"flake": false
},
"b": {
"locked": {
"lastModified": 1700000001,
"narHash": "sha256-BBBB=",
"path": "/tmp/b",
"type": "path"
},
"original": { "type": "path", "url": "/tmp/b" },
"flake": false
},
"root": { "inputs": { "a": "a", "b": "b" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "sha256-AAAA=:sha256-BBBB=");
}
#[test]
#[ignore = "tracked pre-existing behaviour: sui's getFlake exposes `description` at the top-level flake result; CppNix does not. A real (non-env) divergence to fix in getFlake, quarantined so the PR gate stays green until then."]
fn flake_result_has_outpath() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "test";
outputs = { self }: { value = 42; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let has_out = format!(r#"(builtins.getFlake "{flake_path}") ? outPath"#);
let has_desc = format!(r#"(builtins.getFlake "{flake_path}") ? description"#);
let has_val = format!(r#"(builtins.getFlake "{flake_path}") ? value"#);
assert_eq!(eval(&has_out).unwrap(), Value::Bool(true));
assert_eq!(eval(&has_desc).unwrap(), Value::Bool(false));
assert_eq!(eval(&has_val).unwrap(), Value::Bool(true));
}
#[test]
fn flake_result_has_inputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: { ok = true; };
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-INPUTSTEST=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let has_inputs = format!(r#"(builtins.getFlake "{flake_path}") ? inputs"#);
let has_dep = format!(
r#"(builtins.getFlake "{flake_path}").inputs ? dep"#
);
assert_eq!(eval(&has_inputs).unwrap(), Value::Bool(true));
assert_eq!(eval(&has_dep).unwrap(), Value::Bool(true));
}
#[test]
fn flake_inputs_have_outpath() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: {
result = (builtins.getFlake self.outPath).inputs.dep ? outPath;
};
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-DEPOP=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(
r#"(builtins.getFlake "{flake_path}").inputs.dep ? outPath"#
);
assert_eq!(eval(&expr).unwrap(), Value::Bool(true));
}
#[test]
fn flake_self_has_inputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep = { };
outputs = { self, dep }: {
result = self.inputs.dep.narHash;
};
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-SELFIN=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "dep": "dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").result"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "sha256-SELFIN=");
}
#[test]
fn flake_self_outpath_in_outputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
description = "self-test";
outputs = { self }: { dir = self.outPath; };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").dir"#);
let result = eval(&expr).unwrap();
let out = result.as_string().unwrap();
assert!(out.starts_with("/nix/store/"), "self.outPath must be a store path, got {out}");
assert!(out.ends_with("-source"), "self.outPath must end with -source, got {out}");
assert_ne!(out, flake_path);
}
#[test]
fn flake_string_interpolation_with_input() {
let dep_dir = tempfile::tempdir().unwrap();
std::fs::write(
dep_dir.path().join("flake.nix"),
r#"{ description = "dep"; outputs = { self }: { }; }"#,
)
.unwrap();
std::fs::write(dep_dir.path().join("data.txt"), "hello").unwrap();
let dir = tempfile::tempdir().unwrap();
let dep_path = dep_dir.path().to_string_lossy().to_string();
std::fs::write(
dir.path().join("flake.nix"),
format!(
r#"{{
inputs.dep = {{ }};
outputs = {{ self, dep }}: {{
data = builtins.readFile "${{dep}}/data.txt";
}};
}}"#
),
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
format!(
r#"{{
"nodes": {{
"dep": {{
"locked": {{
"lastModified": 1700000000,
"narHash": "sha256-INTERP=",
"path": "{dep_path}",
"type": "path"
}},
"original": {{ "type": "path", "url": "{dep_path}" }}
}},
"root": {{ "inputs": {{ "dep": "dep" }} }}
}},
"root": "root",
"version": 7
}}"#
),
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").data"#);
let result = eval(&expr).unwrap();
assert_eq!(result.as_string().unwrap(), "hello");
}
#[test]
fn flake_result_outpath_is_a_store_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{ outputs = { self }: { }; }"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").outPath"#);
let result = eval(&expr).unwrap();
let out = result.as_string().unwrap();
assert!(out.starts_with("/nix/store/"),
"outPath must start with /nix/store/, got {out}");
assert!(out.ends_with("-source"),
"outPath must end with -source, got {out}");
assert_ne!(out, flake_path,
"outPath must be a store path, not the raw filesystem path");
}
#[test]
fn flake_result_source_info_present() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{ outputs = { self }: { }; }"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}") ? sourceInfo"#);
assert_eq!(eval(&expr).unwrap(), Value::Bool(true));
}
#[test]
fn pure_mode_toggle() {
use crate::eval::{is_pure_mode, set_pure_mode};
set_pure_mode(false);
assert!(!is_pure_mode());
set_pure_mode(true);
assert!(is_pure_mode());
set_pure_mode(false);
assert!(!is_pure_mode());
}
#[test]
fn builtins_to_path() {
let v = ev(r#"builtins.toPath "/foo/bar""#);
assert_eq!(v, Value::Path(Box::new(SmolStr::from("/foo/bar"))));
}
#[test]
fn builtins_to_path_rejects_relative() {
let result = eval(r#"builtins.toPath "relative/path""#);
assert!(result.is_err());
}
#[test]
fn builtins_store_path() {
let v = ev(r#"builtins.storePath "/nix/store/abc-hello""#);
assert_eq!(v, Value::Path(Box::new(SmolStr::from("/nix/store/abc-hello"))));
}
#[test]
fn builtins_store_path_rejects_non_store() {
let result = eval(r#"builtins.storePath "/tmp/not-store""#);
assert!(result.is_err());
}
#[test]
fn builtins_fetch_tarball_from_file() {
let dir = std::env::temp_dir().join("sui_eval_test_fetchtarball");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let tar_gz_path = dir.join("archive.tar.gz");
{
let file = std::fs::File::create(&tar_gz_path).unwrap();
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut tar_builder = tar::Builder::new(enc);
let data = b"hello tarball";
let mut header = tar::Header::new_gnu();
header.set_path("hello.txt").unwrap();
header.set_size(data.len() as u64);
header.set_cksum();
tar_builder.append(&header, &data[..]).unwrap();
tar_builder.finish().unwrap();
}
let file_url = format!("file://{}", tar_gz_path.display());
let expr = format!(r#"builtins.fetchTarball "{}""#, file_url);
let v = eval(&expr).unwrap();
if let Value::Path(p) = v {
assert!(
std::path::Path::new(p.as_str()).exists(),
"extracted dir should exist: {p}",
);
} else {
panic!("expected path, got {v}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_fetch_tarball_bad_type_errors() {
let result = eval("builtins.fetchTarball 42");
assert!(result.is_err());
}
#[test] fn sc_plain() { assert!(!NixString::plain("hello").has_context()); }
#[test] fn sc_merge() { let mut c = StringContext::new(); c.add_plain("/nix/store/abc".to_string()); assert!(NixString::with_context("hi", c).has_context()); }
#[test] fn has_ctx_false() { assert_eq!(ev(r#"builtins.hasContext "hello""#), Value::Bool(false)); }
#[test] fn discard_ctx() { assert_eq!(ev(r#"builtins.hasContext (builtins.unsafeDiscardStringContext "hello")"#), Value::Bool(false)); }
#[test] fn get_ctx_empty() { let v = ev(r#"builtins.getContext "hello""#); if let Value::Attrs(a) = v { assert!(a.is_empty()); } else { panic!(); } }
#[test] fn has_ctx_after_append() { assert_eq!(ev(r#"builtins.hasContext (builtins.appendContext "hello" { "/nix/store/abc" = { path = true; }; })"#), Value::Bool(true)); }
#[test] fn append_ctx_rt() { let v = ev(r#"builtins.getContext (builtins.appendContext "hello" { "/nix/store/abc" = { path = true; }; })"#); if let Value::Attrs(a) = v { assert!(a.contains_key("/nix/store/abc")); } else { panic!(); } }
#[test] fn discard_ctx_all() { let v = ev(r#"let s = builtins.appendContext "hello" { "/nix/store/abc" = { path = true; }; }; clean = builtins.unsafeDiscardStringContext s; in builtins.getContext clean"#); if let Value::Attrs(a) = v { assert!(a.is_empty()); } else { panic!(); } }
#[test] fn concat_merges_ctx() { let v = ev(r#"let a = builtins.appendContext "foo" { "/nix/store/a" = { path = true; }; }; b = builtins.appendContext "bar" { "/nix/store/b" = { path = true; }; }; in builtins.getContext (a + b)"#); if let Value::Attrs(a) = v { assert!(a.contains_key("/nix/store/a")); assert!(a.contains_key("/nix/store/b")); } else { panic!(); } }
#[test]
fn interp_merges_ctx() {
assert_eq!(
ev(r##"let s = builtins.appendContext "world" { "/nix/store/x" = { path = true; }; }; in builtins.hasContext "hello ${s}""##),
Value::Bool(true),
);
}
#[test]
fn path_interp_ctx() {
let dir = std::env::temp_dir().join(format!("sui-r5-ctx-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("c.txt");
std::fs::write(&f, b"x").unwrap();
let v = ev(&format!(r#"builtins.hasContext "${{{}}}""#, f.display()));
assert_eq!(v, Value::Bool(true));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn path_interp_ctx_content() {
let dir = std::env::temp_dir().join(format!("sui-r5-ctxc-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("c.txt");
std::fs::write(&f, b"x").unwrap();
let v = ev(&format!(r#"builtins.getContext "${{{}}}""#, f.display()));
if let Value::Attrs(a) = v {
assert!(!a.is_empty(), "context should contain at least one entry");
} else {
panic!("expected Attrs, got {v:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test] fn add_drv_out_deps() { let v = ev(r#"let s = builtins.appendContext "/nix/store/abc.drv" { "/nix/store/abc.drv" = { path = true; }; }; p = builtins.addDrvOutputDependencies s; in builtins.getContext p"#); if let Value::Attrs(a) = v { let e = a.get("/nix/store/abc.drv").unwrap().as_attrs().unwrap(); assert_eq!(e.get("allOutputs"), Some(&Value::Bool(true))); } else { panic!(); } }
#[test] fn discard_out_dep() { let v = ev(r#"let s = builtins.appendContext "hello" { "/nix/store/x.drv" = { allOutputs = true; }; }; d = builtins.unsafeDiscardOutputDependency s; in builtins.getContext d"#); if let Value::Attrs(a) = v { let e = a.get("/nix/store/x.drv").unwrap().as_attrs().unwrap(); assert_eq!(e.get("path"), Some(&Value::Bool(true))); } else { panic!(); } }
#[test]
fn builtins_generic_closure_linear_chain() {
let v = ev(r#"
builtins.genericClosure {
startSet = [{ key = 1; }];
operator = item: if item.key < 5 then [{ key = item.key + 1; }] else [];
}
"#);
if let Value::List(items) = v {
assert_eq!(items.len(), 5);
for (i, item) in items.iter().enumerate() {
let attrs = item.to_attrs().unwrap();
assert_eq!(attrs.get("key"), Some(&Value::Int(i as i64 + 1)));
}
} else {
panic!("expected list");
}
}
#[test]
fn builtins_generic_closure_diamond_dedup() {
let v = ev(r#"
builtins.genericClosure {
startSet = [{ key = "A"; }];
operator = item:
if item.key == "A" then [{ key = "B"; } { key = "C"; }]
else if item.key == "B" then [{ key = "D"; }]
else if item.key == "C" then [{ key = "D"; }]
else [];
}
"#);
if let Value::List(items) = v {
assert_eq!(items.len(), 4); } else {
panic!("expected list");
}
}
#[test]
fn builtins_generic_closure_empty_operator() {
let v = ev(r#"
builtins.genericClosure {
startSet = [{ key = 1; } { key = 2; }];
operator = item: [];
}
"#);
if let Value::List(items) = v {
assert_eq!(items.len(), 2);
} else {
panic!("expected list");
}
}
#[test]
fn builtins_from_toml_simple_table() {
let v = ev(r#"builtins.fromTOML ''
name = "hello"
version = 42
''"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("name"), Some(&Value::string("hello")));
assert_eq!(a.get("version"), Some(&Value::Int(42)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_from_toml_nested() {
let v = ev(r#"builtins.fromTOML ''
[package]
name = "test"
[package.metadata]
key = true
''"#);
if let Value::Attrs(a) = v {
let pkg = a.get("package").unwrap().as_attrs().unwrap();
assert_eq!(pkg.get("name"), Some(&Value::string("test")));
let meta = pkg.get("metadata").unwrap().as_attrs().unwrap();
assert_eq!(meta.get("key"), Some(&Value::Bool(true)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_from_toml_arrays() {
let v = ev(r#"builtins.fromTOML ''
ports = [80, 443]
''"#);
if let Value::Attrs(a) = v {
assert_eq!(
a.get("ports"),
Some(&Value::list(vec![Value::Int(80), Value::Int(443)])),
);
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_less_than_ints() {
assert_eq!(ev("builtins.lessThan 1 2"), Value::Bool(true));
assert_eq!(ev("builtins.lessThan 2 1"), Value::Bool(false));
assert_eq!(ev("builtins.lessThan 1 1"), Value::Bool(false));
}
#[test]
fn builtins_less_than_floats() {
assert_eq!(ev("builtins.lessThan 1.0 2.0"), Value::Bool(true));
assert_eq!(ev("builtins.lessThan 2.0 1.0"), Value::Bool(false));
}
#[test]
fn builtins_less_than_strings() {
assert_eq!(ev(r#"builtins.lessThan "abc" "def""#), Value::Bool(true));
assert_eq!(ev(r#"builtins.lessThan "def" "abc""#), Value::Bool(false));
}
#[test]
fn builtins_bit_and() {
assert_eq!(ev("builtins.bitAnd 12 10"), Value::Int(8)); }
#[test]
fn builtins_bit_or() {
assert_eq!(ev("builtins.bitOr 12 10"), Value::Int(14)); }
#[test]
fn builtins_bit_xor() {
assert_eq!(ev("builtins.bitXor 12 10"), Value::Int(6)); }
#[test]
fn builtins_split_version_standard() {
assert_eq!(
ev(r#"builtins.splitVersion "1.2.3""#),
Value::list(vec![
Value::string("1"),
Value::string("2"),
Value::string("3"),
]),
);
}
#[test]
fn builtins_split_version_pre_release() {
assert_eq!(
ev(r#"builtins.splitVersion "1.0pre1""#),
Value::list(vec![
Value::string("1"),
Value::string("0"),
Value::string("pre"),
Value::string("1"),
]),
);
}
#[test]
fn builtins_path_exists_tmpfile() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_path_exists.txt");
std::fs::write(&path, "test").unwrap();
let expr = format!(r#"builtins.pathExists "{}""#, path.display());
assert_eq!(eval(&expr).unwrap(), Value::Bool(true));
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_path_exists_nonexistent() {
assert_eq!(
ev(r#"builtins.pathExists "/nonexistent/path/that/surely/does/not/exist""#),
Value::Bool(false),
);
}
#[test]
fn builtins_to_file_returns_store_path() {
let v = ev(r#"builtins.toFile "test.txt" "hello""#);
assert_eq!(
ev(r#"builtins.typeOf (builtins.toFile "test.txt" "hello")"#),
Value::string("string"),
);
let s = v.as_string().unwrap_or_else(|_| panic!("expected string, got {v}"));
assert!(s.starts_with("/nix/store/"));
assert!(s.ends_with("-test.txt"));
}
#[test]
fn builtins_to_file_no_double_copy_in_derivation_env() {
let s = ev(r#"toString (builtins.toFile "myhook" "echo hi")"#)
.as_string()
.unwrap()
.to_string();
let base = s.rsplit('/').next().unwrap();
assert!(base.ends_with("-myhook"), "unexpected name: {base}");
let hash = base.split('-').next().unwrap();
assert_eq!(hash.len(), 32, "store hash must be 32 chars: {base}");
assert!(!base.contains("-myhook-"), "doubled name: {base}");
}
#[test]
fn builtins_to_file_deterministic() {
let v1 = ev(r#"builtins.toFile "f" "content""#);
let v2 = ev(r#"builtins.toFile "f" "content""#);
assert_eq!(v1, v2);
}
#[test]
fn builtins_hash_file_sha256() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_hashfile.txt");
std::fs::write(&path, "hello").unwrap();
let expr = format!(r#"builtins.hashFile "sha256" "{}""#, path.display());
let v = eval(&expr).unwrap();
if let Value::String(ns) = v {
assert_eq!(ns.chars.len(), 64);
assert_eq!(ns.chars, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
} else {
panic!("expected string");
}
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_hash_file_missing() {
let result = eval(r#"builtins.hashFile "sha256" "/nonexistent/file.txt""#);
assert!(result.is_err());
}
#[test]
fn builtins_unsafe_get_attr_pos_returns_null() {
assert_eq!(
ev(r#"builtins.unsafeGetAttrPos "x" { x = 1; }"#),
Value::Null,
);
}
#[test]
fn builtins_store_dir() {
assert_eq!(ev(r#"builtins.storeDir"#), Value::string("/nix/store"));
}
#[test]
#[ignore = "env-dependent: builtins.nixPath reflects the host's NIX_PATH; only [] when NIX_PATH is unset. Run in a clean env with --ignored."]
fn builtins_nix_path_empty() {
assert_eq!(ev(r#"builtins.nixPath"#), Value::list(vec![]));
}
#[test]
fn builtins_find_file_exact_match() {
let dir = std::env::temp_dir().join("sui_eval_test_findfile");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("test.nix"), "42").unwrap();
let expr = format!(
r#"builtins.findFile [{{ prefix = "test.nix"; path = "{}"; }}] "test.nix""#,
dir.join("test.nix").display()
);
let v = eval(&expr).unwrap();
assert!(matches!(v, Value::Path(_)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_find_file_not_found() {
let result = eval(r#"builtins.findFile [] "nonexistent""#);
assert!(result.is_err());
}
#[test] fn builtins_generic_closure_linear() {
assert_eq!(ev(r#"builtins.length (builtins.genericClosure { startSet = [{ key = 1; }]; operator = item: if item.key < 3 then [{ key = item.key + 1; }] else []; })"#), Value::Int(3));
}
#[test] fn builtins_generic_closure_empty_op() {
assert_eq!(ev(r#"builtins.length (builtins.genericClosure { startSet = [{ key = 1; }]; operator = item: []; })"#), Value::Int(1));
}
#[test] fn builtins_generic_closure_dedup() {
assert_eq!(ev(r#"builtins.length (builtins.genericClosure { startSet = [{ key = 1; }]; operator = item: [{ key = 1; } { key = 2; }]; })"#), Value::Int(2));
}
#[test] fn builtins_from_toml_simple() {
let v = ev(r#"builtins.fromTOML "[section]\nkey = \"value\"""#);
if let Value::Attrs(a) = v { assert!(a.contains_key("section")); } else { panic!(); }
}
#[test] fn builtins_less_than_int() {
assert_eq!(ev("builtins.lessThan 1 2"), Value::Bool(true));
assert_eq!(ev("builtins.lessThan 2 1"), Value::Bool(false));
}
#[test] fn builtins_bit_and_12_10() { assert_eq!(ev("builtins.bitAnd 12 10"), Value::Int(8)); }
#[test] fn builtins_bit_or_12_10() { assert_eq!(ev("builtins.bitOr 12 10"), Value::Int(14)); }
#[test] fn builtins_bit_xor_12_10() { assert_eq!(ev("builtins.bitXor 12 10"), Value::Int(6)); }
#[test] fn builtins_split_version() {
assert_eq!(ev(r#"builtins.splitVersion "1.2.3""#), Value::list(vec![
Value::string("1"), Value::string("2"), Value::string("3")
]));
}
#[test] fn builtins_split_version_pre() {
let v = ev(r#"builtins.splitVersion "1pre2""#);
if let Value::List(l) = v { assert!(l.len() >= 3); } else { panic!(); }
}
#[test] fn builtins_path_exists_false() {
assert_eq!(ev(r#"builtins.pathExists "/nonexistent/path/12345""#), Value::Bool(false));
}
#[test] fn builtins_to_file() {
let v = ev(r#"builtins.toFile "test.txt" "hello""#);
assert!(matches!(v, Value::String(_)), "expected string, got {v}");
}
#[test] fn builtins_unsafe_get_attr_pos() {
assert_eq!(ev(r#"builtins.unsafeGetAttrPos "a" { a = 1; }"#), Value::Null);
}
#[test] fn builtins_derivation_strict() {
let v = ev(r#"builtins.derivationStrict { name = "test"; system = "x86_64-linux"; builder = "/bin/sh"; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("type").unwrap().as_string().unwrap(), "derivation");
assert!(a.contains_key("drvPath"));
} else { panic!(); }
}
#[test] fn builtins_to_xml_int() {
let v = ev("builtins.toXML 42");
let s = v.as_string().unwrap();
assert!(s.contains("<int value=\"42\""));
}
#[test] fn builtins_to_xml_attrs() {
let v = ev(r#"builtins.toXML { a = 1; }"#);
let s = v.as_string().unwrap();
assert!(s.contains("<attrs>"));
assert!(s.contains("attr name=\"a\""));
}
#[test]
fn builtins_sub_ints() {
assert_eq!(ev("builtins.sub 10 3"), Value::Int(7));
}
#[test]
fn builtins_mul_ints() {
assert_eq!(ev("builtins.mul 4 5"), Value::Int(20));
}
#[test]
fn builtins_div_ints() {
assert_eq!(ev("builtins.div 10 3"), Value::Int(3));
}
#[test]
fn builtins_div_by_zero() {
let result = eval("builtins.div 10 0");
assert!(result.is_err());
}
#[test]
fn builtins_add_ints() {
assert_eq!(ev("builtins.add 3 4"), Value::Int(7));
}
#[test]
fn builtins_add_floats() {
assert_eq!(ev("builtins.add 1.5 2.5"), Value::Float(4.0));
}
#[test]
fn builtins_add_mixed_int_float() {
assert_eq!(ev("builtins.add 1 2.5"), Value::Float(3.5));
}
#[test]
fn builtins_is_float_true() {
assert_eq!(ev("builtins.isFloat 1.0"), Value::Bool(true));
}
#[test]
fn builtins_is_float_false() {
assert_eq!(ev("builtins.isFloat 1"), Value::Bool(false));
}
#[test]
fn builtins_deep_seq() {
assert_eq!(ev("builtins.deepSeq [1 2 3] 42"), Value::Int(42));
}
#[test]
fn builtins_deep_seq_with_attrs() {
assert_eq!(ev(r#"builtins.deepSeq { a = 1; b = 2; } "ok""#), Value::string("ok"));
}
#[test]
fn builtins_deep_seq_cyclic_attrset_terminates() {
assert_eq!(
ev("builtins.deepSeq (let as = { x = 123; y = as; }; in as) 456"),
Value::Int(456)
);
}
#[test]
fn builtins_deep_seq_cyclic_list_terminates() {
assert_eq!(
ev("builtins.deepSeq (let xs = [ 1 xs ]; in xs) 7"),
Value::Int(7)
);
}
#[test]
fn builtins_deep_seq_still_forces_a_nested_throw() {
let result = eval(r#"builtins.deepSeq { a = throw "boom"; } 1"#);
assert!(result.is_err(), "deepSeq must force the nested throw, got {result:?}");
assert!(format!("{}", result.unwrap_err()).contains("boom"));
}
#[test]
fn builtins_get_env_missing() {
assert_eq!(
ev(r#"builtins.getEnv "DEFINITELY_NOT_SET_12345_XYZ""#),
Value::string(""),
);
}
#[test]
fn builtins_current_time_is_int() {
let v = ev("builtins.currentTime null");
assert!(matches!(v, Value::Int(_)));
if let Value::Int(t) = v {
assert!(t > 0);
}
}
#[test]
fn builtins_substring_basic() {
assert_eq!(
ev(r#"builtins.substring 0 5 "hello world""#),
Value::string("hello"),
);
}
#[test]
fn builtins_substring_from_middle() {
assert_eq!(
ev(r#"builtins.substring 6 5 "hello world""#),
Value::string("world"),
);
}
#[test]
fn builtins_substring_beyond_length() {
assert_eq!(
ev(r#"builtins.substring 0 100 "hi""#),
Value::string("hi"),
);
}
#[test]
fn builtins_split_basic() {
let v = ev(r#"builtins.split "o" "foobar""#);
if let Value::List(parts) = v {
assert!(parts.len() >= 3);
} else {
panic!("expected list");
}
}
#[test]
fn builtins_has_context_plain_string() {
assert_eq!(ev(r#"builtins.hasContext "hello""#), Value::Bool(false));
}
#[test]
fn builtins_get_context_plain_string() {
let v = ev(r#"builtins.getContext "hello""#);
if let Value::Attrs(a) = v {
assert!(a.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_unsafe_discard_string_context() {
assert_eq!(
ev(r#"builtins.unsafeDiscardStringContext "hello""#),
Value::string("hello"),
);
}
#[test]
fn builtins_unsafe_discard_output_dependency() {
assert_eq!(
ev(r#"builtins.unsafeDiscardOutputDependency "hello""#),
Value::string("hello"),
);
}
#[test]
fn builtins_append_context_empty() {
assert_eq!(
ev(r#"builtins.appendContext "hello" {}"#),
Value::string("hello"),
);
}
#[test]
fn builtins_convert_hash_sha256_hex_to_base64() {
let v = ev(r#"builtins.convertHash { hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; hashAlgo = "sha256"; toHashFormat = "base64"; }"#);
if let Value::String(s) = v {
assert!(!s.chars.is_empty());
} else {
panic!("expected string");
}
}
#[test]
fn builtins_convert_hash_sha256_hex_to_sri() {
let v = ev(r#"builtins.convertHash { hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; hashAlgo = "sha256"; toHashFormat = "sri"; }"#);
if let Value::String(s) = v {
assert!(s.chars.starts_with("sha256-"));
} else {
panic!("expected string");
}
}
#[test]
fn builtins_to_xml_string() {
let v = ev(r#"builtins.toXML "hello""#);
let s = v.as_string().unwrap();
assert!(s.contains("<string value="));
}
#[test]
fn builtins_to_xml_list() {
let v = ev("builtins.toXML [1 2]");
let s = v.as_string().unwrap();
assert!(s.contains("<list>"));
}
#[test]
fn builtins_to_xml_bool() {
let v = ev("builtins.toXML true");
let s = v.as_string().unwrap();
assert!(s.contains("<bool value=\"true\""));
}
#[test]
fn builtins_to_xml_null() {
let v = ev("builtins.toXML null");
let s = v.as_string().unwrap();
assert!(s.contains("<null"));
}
#[test]
fn builtins_type_of_int() {
assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
}
#[test]
fn builtins_type_of_float() {
assert_eq!(ev("builtins.typeOf 3.14"), Value::string("float"));
}
#[test]
fn builtins_type_of_string() {
assert_eq!(ev(r#"builtins.typeOf "hello""#), Value::string("string"));
}
#[test]
fn builtins_type_of_bool() {
assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
}
#[test]
fn builtins_type_of_null() {
assert_eq!(ev("builtins.typeOf null"), Value::string("null"));
}
#[test]
fn builtins_type_of_list() {
assert_eq!(ev("builtins.typeOf [1 2]"), Value::string("list"));
}
#[test]
fn builtins_type_of_set() {
assert_eq!(ev("builtins.typeOf { a = 1; }"), Value::string("set"));
}
#[test]
fn builtins_type_of_lambda() {
assert_eq!(ev("builtins.typeOf (x: x)"), Value::string("lambda"));
}
#[test]
fn builtins_type_of_path() {
assert_eq!(ev("builtins.typeOf /foo"), Value::string("path"));
}
#[test]
fn builtins_head_single() {
assert_eq!(ev("builtins.head [42]"), Value::Int(42));
}
#[test]
fn builtins_head_empty_errors() {
assert!(eval("builtins.head []").is_err());
}
#[test]
fn builtins_tail_single() {
assert_eq!(ev("builtins.tail [42]"), Value::list(vec![]));
}
#[test]
fn builtins_tail_empty_errors() {
assert!(eval("builtins.tail []").is_err());
}
#[test]
fn builtins_attr_names_sorted() {
assert_eq!(
ev(r#"builtins.attrNames { z = 1; a = 2; m = 3; }"#),
Value::list(vec![
Value::string("a"),
Value::string("m"),
Value::string("z"),
]),
);
}
#[test]
fn builtins_attr_values_follows_sorted_keys() {
assert_eq!(
ev(r#"builtins.attrValues { z = 3; a = 1; m = 2; }"#),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn builtins_to_string_int() {
assert_eq!(ev("builtins.toString 42"), Value::string("42"));
}
#[test]
fn builtins_to_string_bool() {
assert_eq!(ev("builtins.toString true"), Value::string("1"));
assert_eq!(ev("builtins.toString false"), Value::string(""));
}
#[test]
fn builtins_to_string_null() {
assert_eq!(ev("builtins.toString null"), Value::string(""));
}
#[test]
fn builtins_to_string_path() {
assert_eq!(ev("builtins.toString /foo"), Value::string("/foo"));
}
#[test]
fn builtins_to_string_list_space_joined() {
assert_eq!(
ev("builtins.toString [1 2 3]"),
Value::string("1 2 3"),
);
}
#[test]
fn builtins_to_string_outpath() {
assert_eq!(
ev(r#"builtins.toString { outPath = "/nix/store/xyz"; }"#),
Value::string("/nix/store/xyz"),
);
}
#[test]
fn builtins_to_string_tostring_over_outpath() {
assert_eq!(
ev(r#"builtins.toString { __toString = self: "win"; outPath = "/lose"; }"#),
Value::string("win"),
);
}
#[test]
fn builtins_abort_produces_error() {
let result = eval(r#"builtins.abort "fatal""#);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("fatal"));
}
#[test]
fn builtins_from_json_null() {
assert_eq!(ev(r#"builtins.fromJSON "null""#), Value::Null);
}
#[test]
fn builtins_from_json_bool() {
assert_eq!(ev(r#"builtins.fromJSON "true""#), Value::Bool(true));
}
#[test]
fn builtins_from_json_list() {
assert_eq!(
ev(r#"builtins.fromJSON "[1,2,3]""#),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn builtins_to_json_null() {
assert_eq!(ev("builtins.toJSON null"), Value::string("null"));
}
#[test]
fn to_json_preserves_string_context() {
let v = ev(
r#"builtins.getContext (builtins.toJSON (
builtins.appendContext "s" { "/nix/store/abc.drv" = { path = true; }; }))"#,
);
if let Value::Attrs(a) = v {
assert!(a.contains_key("/nix/store/abc.drv"), "toJSON dropped context");
} else {
panic!("expected attrs");
}
let v2 = ev(
r#"builtins.getContext (builtins.toJSON [
(builtins.appendContext "s" { "/nix/store/abc.drv" = { path = true; }; }) ])"#,
);
if let Value::Attrs(a) = v2 {
assert!(a.contains_key("/nix/store/abc.drv"), "toJSON dropped list-element context");
} else {
panic!("expected attrs");
}
}
#[test]
fn to_json_collapses_derivation_to_outpath() {
assert_eq!(
ev(r#"builtins.toJSON { type = "derivation"; outPath = "/nix/store/x"; name = "x"; }"#),
Value::string(r#""/nix/store/x""#)
);
assert_eq!(
ev(r#"builtins.toJSON { __toString = _: "hi"; other = 1; }"#),
Value::string(r#""hi""#)
);
}
#[test]
fn bare_globals_include_placeholder_and_fetchers() {
for g in [
"placeholder",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fromTOML",
] {
assert_eq!(
ev(&format!("builtins.typeOf {g}")),
Value::string("lambda"),
"bare global `{g}` should resolve to a lambda"
);
}
assert!(matches!(ev(r#"placeholder "out""#), Value::String(_)));
}
#[test]
fn placeholder_byte_matches_cppnix() {
assert_eq!(
ev(r#"builtins.placeholder "out""#),
Value::string("/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"),
);
assert_eq!(
ev(r#"builtins.placeholder "dev""#),
Value::string("/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz"),
);
}
#[test]
fn multi_output_drv_byte_matches_cppnix() {
assert_eq!(
ev(r#"(derivation { name = "m"; system = "aarch64-darwin"; builder = "/bin/sh"; args = [ "-c" "x" ]; outputs = [ "out" "bin" "dev" ]; }).drvPath"#),
Value::string("/nix/store/g76j4rxpbj2f04hjpnj1fhgz4d8pbj3n-m.drv"),
);
}
#[test]
fn default_output_is_first_declared() {
assert_eq!(
ev(r#"(derivation { name = "d"; system = "x86_64-linux"; builder = "/bin/sh"; outputs = [ "bin" "out" "dev" ]; }).outputName"#),
Value::string("bin"),
);
assert_eq!(
ev(r#"(derivation { name = "d"; system = "x86_64-linux"; builder = "/bin/sh"; }).outputName"#),
Value::string("out"),
);
}
#[test]
fn ignore_nulls_and_builder_context_byte_match_cppnix() {
assert_eq!(
ev(r#"let a = derivation { name = "a"; system = "aarch64-darwin"; builder = "/bin/sh"; args = [ "-c" "echo hi > $out" ]; }; b = derivation { name = "b"; system = "aarch64-darwin"; builder = "${a}/bin/sh"; __ignoreNulls = true; keep = "v"; drop = null; }; in b.drvPath"#),
Value::string("/nix/store/442cqmi3055irhx2r2z70l4isxmvzs37-b.drv"),
);
}
#[test]
fn fod_drv_byte_matches_cppnix() {
assert_eq!(
ev(r#"(builtins.derivation { name = "s"; system = "aarch64-darwin"; builder = "/bin/sh"; outputHash = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2"; outputHashAlgo = "sha256"; outputHashMode = "flat"; }).drvPath"#),
Value::string("/nix/store/yswzdrpndzk7a21fyzdxdkly4s74xpra-s.drv"),
);
}
#[test]
fn concat_strings_sep_preserves_context_byte_matches_cppnix() {
assert_eq!(
ev(r#"let m = derivation { name = "m"; system = "x86_64-linux"; builder = "/bin/sh"; outputs = [ "out" "dev" ]; }; in (derivation { name = "c"; system = "x86_64-linux"; builder = "/bin/sh"; L = builtins.concatStringsSep ":" [ "${m.out}/lib" "${m.dev}/include" ]; }).drvPath"#),
Value::string("/nix/store/6w30qnbkalsil8yd09680vzkzczbzhjv-c.drv"),
);
}
#[test]
fn attrset_dotted_and_fullset_binding_merge() {
assert_eq!(ev(r#"let s = { a.b = "x"; a = { c = "y"; }; }; in s.a.b + s.a.c"#), Value::string("xy"));
assert_eq!(ev(r#"let s = { a.b.c = "1"; a = { d = "2"; }; a.b.e = "3"; }; in s.a.b.c + s.a.b.e + s.a.d"#), Value::string("132"));
}
#[test]
fn fod_consumer_output_modulo_byte_matches_cppnix() {
sui_spec::derivation::reset_modulo_cache();
assert_eq!(
ev(r#"let fod = builtins.derivation { name = "f"; system = "aarch64-darwin"; builder = "/bin/sh"; outputHash = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2"; outputHashAlgo = "sha256"; outputHashMode = "flat"; }; consumer = builtins.derivation { name = "c"; system = "aarch64-darwin"; builder = "/bin/sh"; args = [ fod ]; }; in consumer.outPath"#),
Value::string("/nix/store/y624jf64a4mkfy8rgcxln3wnf28nsg4r-c"),
);
}
#[test]
fn fod_with_input_drv_folds_refs_byte_matches_cppnix() {
sui_spec::derivation::reset_modulo_cache();
assert_eq!(
ev(r#"let dep = builtins.derivation { name = "dep"; system = "aarch64-darwin"; builder = "/bin/sh"; }; fod = builtins.derivation { name = "ff"; system = "aarch64-darwin"; builder = "${dep}/bin/sh"; outputHash = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2"; outputHashAlgo = "sha256"; outputHashMode = "flat"; }; in fod.drvPath"#),
Value::string("/nix/store/68za8mj2yl3db3qzcqyamg5yhlf4nczs-ff.drv"),
);
}
#[test]
fn two_input_consumer_modulo_sort_byte_matches_cppnix() {
sui_spec::derivation::reset_modulo_cache();
assert_eq!(
ev(r#"let a = builtins.derivation { name = "a"; system = "aarch64-darwin"; builder = "/bin/sh"; }; b = builtins.derivation { name = "b"; system = "aarch64-darwin"; builder = "/bin/sh"; }; c = builtins.derivation { name = "c"; system = "aarch64-darwin"; builder = "/bin/sh"; args = [ a b ]; }; in c.outPath"#),
Value::string("/nix/store/wdw53pm18752ihdihmdsl6asy616cyk3-c"),
);
}
#[test]
fn structured_attrs_byte_matches_cppnix() {
sui_spec::derivation::reset_modulo_cache();
assert_eq!(
ev(r#"(builtins.derivation { name = "s"; system = "aarch64-darwin"; builder = "/bin/sh"; __structuredAttrs = true; foo = "bar"; nums = [ 1 2 ]; flag = true; }).drvPath"#),
Value::string("/nix/store/ajlwk64l5a2knarz6nxbhv8z1k3453k0-s.drv"),
);
}
#[test]
fn structured_attrs_with_outputs_args_null_byte_matches_cppnix() {
sui_spec::derivation::reset_modulo_cache();
assert_eq!(
ev(r#"(builtins.derivation { name = "s3"; system = "aarch64-darwin"; builder = "/bin/sh"; args = [ "-e" "x" ]; outputs = [ "out" "dev" ]; __structuredAttrs = true; foo = "bar"; nn = null; }).drvPath"#),
Value::string("/nix/store/kmfpxabrms6bf9gx7rcclpyl3gacjsmv-s3.drv"),
);
}
#[test]
fn builtins_to_json_list() {
assert_eq!(
ev("builtins.toJSON [1 2 3]"),
Value::string("[1,2,3]"),
);
}
#[test]
fn builtins_string_length_empty() {
assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
}
#[test]
fn builtins_string_length_unicode() {
assert_eq!(ev(r#"builtins.stringLength "abc""#), Value::Int(3));
}
#[test]
fn builtins_replace_strings_empty_from() {
assert_eq!(
ev(r#"builtins.replaceStrings [] [] "hello""#),
Value::string("hello"),
);
}
#[test]
fn builtins_replace_strings_no_match() {
assert_eq!(
ev(r#"builtins.replaceStrings ["x"] ["y"] "hello""#),
Value::string("hello"),
);
}
#[test]
fn builtins_replace_strings_first_match_wins() {
assert_eq!(
ev(r#"builtins.replaceStrings ["a" "ab"] ["X" "Y"] "abc""#),
Value::string("Xbc"),
);
assert_eq!(
ev(r#"builtins.replaceStrings ["oo" "o"] ["1" "2"] "foo""#),
Value::string("f1"),
);
}
#[test]
fn builtins_replace_strings_empty_from_interleaves() {
assert_eq!(
ev(r#"builtins.replaceStrings [""] ["-"] "abc""#),
Value::string("-a-b-c-"),
);
}
#[test]
fn builtins_replace_strings_preserves_subject_context() {
let ctx_len = ev(
r#"let d = derivation { name="ctxdep"; system="x86_64-linux"; builder="/bin/sh"; };
s = "-B${d}/bin";
in builtins.length (builtins.attrNames (builtins.getContext
(builtins.replaceStrings ["ZZZ"] ["_"] s)))"#,
);
assert_eq!(ctx_len, Value::Int(1));
}
#[test]
fn builtins_replace_strings_merges_replacement_context() {
let ctx_len = ev(
r#"let d = derivation { name="rep"; system="x86_64-linux"; builder="/bin/sh"; };
in builtins.length (builtins.attrNames (builtins.getContext
(builtins.replaceStrings ["X"] ["${d}"] "aXb")))"#,
);
assert_eq!(ctx_len, Value::Int(1));
}
#[test]
fn builtins_warn_returns_value() {
assert_eq!(ev(r#"builtins.warn "msg" 42"#), Value::Int(42));
}
#[test]
fn builtins_warn_passes_through_attrs() {
let v = ev(r#"builtins.warn "be careful" { a = 1; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(1)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_warn_non_string_message_errors() {
let result = eval("builtins.warn 1 2");
assert!(result.is_err());
}
#[test]
fn builtins_trace_verbose_returns_value() {
assert_eq!(ev(r#"builtins.traceVerbose "msg" 42"#), Value::Int(42));
}
#[test]
fn builtins_trace_verbose_with_attrs() {
let v = ev(r#"builtins.traceVerbose "x" { y = 7; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("y"), Some(&Value::Int(7)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_trace_verbose_with_list() {
assert_eq!(
ev(r#"builtins.traceVerbose "x" [1 2]"#),
Value::list(vec![Value::Int(1), Value::Int(2)]),
);
}
#[test]
fn builtins_break_returns_int() {
assert_eq!(ev("builtins.break 42"), Value::Int(42));
}
#[test]
fn builtins_break_returns_string() {
assert_eq!(ev(r#"builtins.break "x""#), Value::string("x"));
}
#[test]
fn builtins_break_returns_attrs() {
let v = ev(r#"builtins.break { a = 1; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(1)));
} else {
panic!("expected attrs");
}
}
fn make_local_git_repo() -> Option<std::path::PathBuf> {
let dir = std::env::temp_dir().join(format!(
"sui_eval_local_git_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).ok()?;
let repo = crate::git::init_repo(&dir, "main").ok()?;
crate::git::set_config(&repo, "user.email", "test@sui.local").ok()?;
crate::git::set_config(&repo, "user.name", "sui-test").ok()?;
std::fs::write(dir.join("README"), "hello").ok()?;
crate::git::commit_all(&repo, "initial", "sui-test", "test@sui.local").ok()?;
Some(dir)
}
#[test]
fn builtins_fetch_git_local_repo() {
let Some(repo) = make_local_git_repo() else {
eprintln!("skip: git not available");
return;
};
let expr = format!(r#"builtins.fetchGit "{}""#, repo.display());
let v = eval(&expr).unwrap();
if let Value::Attrs(a) = v {
assert!(a.contains_key("outPath"), "outPath missing");
assert!(a.contains_key("rev"), "rev missing");
assert!(a.contains_key("shortRev"), "shortRev missing");
assert!(a.contains_key("revCount"), "revCount missing");
assert!(a.contains_key("lastModified"), "lastModified missing");
assert!(a.contains_key("lastModifiedDate"), "lastModifiedDate missing");
assert!(a.contains_key("narHash"), "narHash missing");
assert!(a.contains_key("submodules"), "submodules missing");
let rev = a.get("rev").unwrap().as_string().unwrap();
let short = a.get("shortRev").unwrap().as_string().unwrap();
assert_eq!(short, rev[..7].to_string());
} else {
panic!("expected attrs");
}
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn builtins_fetch_git_attrset_form() {
let Some(repo) = make_local_git_repo() else {
eprintln!("skip: git not available");
return;
};
let expr = format!(
r#"builtins.fetchGit {{ url = "{}"; }}"#,
repo.display()
);
let v = eval(&expr).unwrap();
assert!(matches!(v, Value::Attrs(_)));
let _ = std::fs::remove_dir_all(&repo);
}
#[test]
fn builtins_fetch_git_invalid_input_errors() {
let result = eval("builtins.fetchGit 42");
assert!(result.is_err());
}
#[test]
fn builtins_fetch_tree_path_type() {
let dir = std::env::temp_dir().join("sui_fetch_tree_path");
std::fs::create_dir_all(&dir).unwrap();
let expr = format!(
r#"(builtins.fetchTree {{ type = "path"; path = "{}"; }}).outPath"#,
dir.display()
);
let v = eval(&expr).unwrap();
if let Value::Path(p) = v {
assert_eq!(p.as_str(), dir.to_string_lossy().as_ref());
} else {
panic!("expected path, got {v}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_fetch_tree_unknown_type_errors() {
let result = eval(r#"builtins.fetchTree { type = "borp"; }"#);
assert!(result.is_err());
}
#[test]
fn builtins_fetch_mercurial_unsupported_input_errors() {
let result = eval("builtins.fetchMercurial 42");
assert!(result.is_err());
}
#[test]
fn builtins_format_unix_yyyymmddhhmmss_basic() {
assert_eq!(super::format_unix_yyyymmddhhmmss(1_704_067_200), "20240101000000");
assert_eq!(super::format_unix_yyyymmddhhmmss(0), "19700101000000");
assert_eq!(super::format_unix_yyyymmddhhmmss(1_775_478_896), "20260406123456");
}
#[test]
fn builtins_filter_source_keeps_all_returns_store_path() {
let dir = std::env::temp_dir().join("sui_eval_filter_src_keep");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.txt"), "alpha").unwrap();
std::fs::write(dir.join("b.txt"), "beta").unwrap();
let expr = format!(
r#"builtins.filterSource (path: type: true) "{}""#,
dir.display()
);
let v = eval(&expr).unwrap();
if let Value::Path(p) = v {
assert!(
p.as_str().starts_with("/nix/store/"),
"filterSource must return a /nix/store path, got {p}"
);
assert!(p.as_str().ends_with("-sui_eval_filter_src_keep"));
} else {
panic!("expected path");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_filter_source_filters_by_predicate() {
let dir = std::env::temp_dir().join("sui_eval_filter_src_pred");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("keep.txt"), "k").unwrap();
std::fs::write(dir.join("drop.txt"), "d").unwrap();
let filtered = match eval(&format!(
r#"builtins.filterSource (path: type: type == "directory" || (builtins.match ".*keep.*" path != null)) "{}""#,
dir.display()
)).unwrap() {
Value::Path(p) => p.as_str().to_string(),
_ => panic!("expected path"),
};
let keep_all = match eval(&format!(
r#"builtins.filterSource (path: type: true) "{}""#,
dir.display()
)).unwrap() {
Value::Path(p) => p.as_str().to_string(),
_ => panic!("expected path"),
};
assert!(filtered.starts_with("/nix/store/"), "got {filtered}");
assert_ne!(
filtered, keep_all,
"filtered tree (drop.txt excluded) must hash differently from the keep-all tree"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn builtins_filter_source_missing_path_errors() {
let result = eval(
r#"builtins.filterSource (path: type: true) "/nonexistent/sui_filter_src_xyz""#,
);
assert!(result.is_err());
}
#[test]
fn builtins_scoped_import_injects_scope() {
let dir = std::env::temp_dir().join("sui_eval_scoped_import");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("inject.nix");
std::fs::write(&path, "foo + 1").unwrap();
let expr = format!(
r#"builtins.scopedImport {{ foo = 41; }} "{}""#,
path.display()
);
assert_eq!(eval(&expr).unwrap(), Value::Int(42));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn builtins_scoped_import_returns_attrs() {
let dir = std::env::temp_dir().join("sui_eval_scoped_import_attrs");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("attrs.nix");
std::fs::write(&path, "{ x = bar; y = bar + 1; }").unwrap();
let expr = format!(
r#"builtins.scopedImport {{ bar = 7; }} "{}""#,
path.display()
);
let v = eval(&expr).unwrap();
if let Value::Attrs(a) = v {
assert_eq!(a.get("x"), Some(&Value::Int(7)));
assert_eq!(a.get("y"), Some(&Value::Int(8)));
} else {
panic!("expected attrs");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn builtins_scoped_import_missing_path_errors() {
let result = eval(
r#"builtins.scopedImport { foo = 1; } "/nonexistent/scoped/import.nix""#,
);
assert!(result.is_err());
}
#[test]
fn builtins_scoped_import_first_arg_must_be_attrs() {
let result = eval(r#"builtins.scopedImport "not-attrs" "/tmp/foo.nix""#);
assert!(result.is_err());
}
#[test]
fn builtins_parse_flake_ref_github_basic() {
let v = ev(r#"builtins.parseFlakeRef "github:NixOS/nixpkgs""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("type").unwrap().as_string().unwrap(), "github");
assert_eq!(a.get("owner").unwrap().as_string().unwrap(), "NixOS");
assert_eq!(a.get("repo").unwrap().as_string().unwrap(), "nixpkgs");
assert!(a.get("ref").is_none());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_flake_ref_github_with_ref() {
let v = ev(r#"builtins.parseFlakeRef "github:NixOS/nixpkgs/release-23.11""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("ref").unwrap().as_string().unwrap(), "release-23.11");
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_flake_ref_git_with_query() {
let v = ev(r#"builtins.parseFlakeRef "git+https://example.com/foo?ref=main""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("type").unwrap().as_string().unwrap(), "git");
assert_eq!(a.get("url").unwrap().as_string().unwrap(), "https://example.com/foo");
assert_eq!(a.get("ref").unwrap().as_string().unwrap(), "main");
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_flake_ref_path_explicit() {
let v = ev(r#"builtins.parseFlakeRef "path:/tmp/foo""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("type").unwrap().as_string().unwrap(), "path");
assert_eq!(a.get("path").unwrap().as_string().unwrap(), "/tmp/foo");
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_parse_flake_ref_invalid_errors() {
let result = eval(r#"builtins.parseFlakeRef "bogus:schema:more""#);
assert!(result.is_err());
}
#[test]
fn builtins_flake_ref_to_string_github_basic() {
assert_eq!(
ev(r#"builtins.flakeRefToString { type = "github"; owner = "NixOS"; repo = "nixpkgs"; }"#),
Value::string("github:NixOS/nixpkgs"),
);
}
#[test]
fn builtins_flake_ref_to_string_github_with_ref() {
assert_eq!(
ev(r#"builtins.flakeRefToString { type = "github"; owner = "NixOS"; repo = "nixpkgs"; ref = "release-23.11"; }"#),
Value::string("github:NixOS/nixpkgs/release-23.11"),
);
}
#[test]
fn builtins_flake_ref_to_string_git_with_query() {
assert_eq!(
ev(r#"builtins.flakeRefToString { type = "git"; url = "https://example.com/foo"; ref = "main"; }"#),
Value::string("git+https://example.com/foo?ref=main"),
);
}
#[test]
fn builtins_flake_ref_to_string_path() {
assert_eq!(
ev(r#"builtins.flakeRefToString { type = "path"; path = "/tmp/foo"; }"#),
Value::string("path:/tmp/foo"),
);
}
#[test]
fn builtins_flake_ref_to_string_unknown_type_errors() {
let result = eval(r#"builtins.flakeRefToString { type = "borp"; }"#);
assert!(result.is_err());
}
#[test]
fn builtins_flake_ref_round_trip() {
assert_eq!(
ev(r#"builtins.flakeRefToString (builtins.parseFlakeRef "github:NixOS/nixpkgs")"#),
Value::string("github:NixOS/nixpkgs"),
);
}
#[test]
fn builtins_filter_attrs_keeps_matching() {
let v = ev(r#"builtins.filterAttrs (n: v: v > 1) { a = 1; b = 2; c = 3; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.len(), 2);
assert_eq!(a.get("b"), Some(&Value::Int(2)));
assert_eq!(a.get("c"), Some(&Value::Int(3)));
assert!(a.get("a").is_none());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_filter_attrs_by_name() {
let v = ev(r#"builtins.filterAttrs (n: v: n == "keep") { keep = 1; drop = 2; }"#);
if let Value::Attrs(a) = v {
assert_eq!(a.len(), 1);
assert_eq!(a.get("keep"), Some(&Value::Int(1)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_filter_attrs_empty() {
let v = ev(r#"builtins.filterAttrs (n: v: true) {}"#);
if let Value::Attrs(a) = v {
assert!(a.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_filter_attrs_non_attrs_errors() {
let result = eval(r#"builtins.filterAttrs (n: v: true) [1 2 3]"#);
assert!(result.is_err());
}
#[test]
fn filter_attrs_result_is_order_independent() {
let v = ev(r#"builtins.filterAttrs (_: v: (v / 2) * 2 == v)
{ a = 1; b = 2; c = 3; d = 4; e = 5; f = 6; g = 7; h = 8; i = 9; j = 10; k = 11; l = 12; }"#);
if let Value::Attrs(a) = v {
let mut kept: Vec<String> = a.iter().map(|(k, _)| k).collect();
kept.sort();
assert_eq!(kept, vec!["b", "d", "f", "h", "j", "l"]);
assert_eq!(a.get("b"), Some(&Value::Int(2)));
assert_eq!(a.get("l"), Some(&Value::Int(12)));
assert!(a.get("a").is_none());
} else {
panic!("expected attrs");
}
}
#[test]
fn filter_attrs_no_cross_entry_force_dependency() {
let by_val = ev(r#"builtins.filterAttrs (_: v: v == "yes") { a = "yes"; b = "no"; c = "yes"; }"#);
let by_key = ev(r#"builtins.filterAttrs (k: _: k == "a" || k == "c") { a = "yes"; b = "no"; c = "yes"; }"#);
let names = |v: &Value| -> Vec<String> {
if let Value::Attrs(a) = v {
let mut n: Vec<String> = a.iter().map(|(k, _)| k).collect();
n.sort();
n
} else {
panic!("expected attrs");
}
};
assert_eq!(names(&by_val), vec!["a", "c"]);
assert_eq!(names(&by_val), names(&by_key));
}
#[test]
fn sui_ext_namespace_exists() {
assert_eq!(ev("builtins ? sui"), Value::Bool(true));
}
#[test]
fn sui_ext_blake3_known_vector() {
assert_eq!(
ev(r#"builtins.sui.blake3 """#),
Value::string("af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"),
);
}
#[test]
fn sui_ext_blake3_hello() {
let v = ev(r#"builtins.sui.blake3 "hello""#);
if let Value::String(s) = v {
assert_eq!(s.chars.len(), 64);
} else { panic!(); }
}
#[test]
fn sui_ext_blake3_non_string_errors() {
let result = eval("builtins.sui.blake3 42");
assert!(result.is_err());
}
#[test]
fn sui_ext_sha3_256_known_vector() {
assert_eq!(
ev(r#"builtins.sui.sha3_256 """#),
Value::string("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"),
);
}
#[test]
fn sui_ext_sha3_256_hello() {
let v = ev(r#"builtins.sui.sha3_256 "hello""#);
if let Value::String(s) = v { assert_eq!(s.chars.len(), 64); } else { panic!(); }
}
#[test]
fn sui_ext_sha3_512_known_vector() {
assert_eq!(
ev(r#"builtins.sui.sha3_512 """#),
Value::string("a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"),
);
}
#[test]
fn sui_ext_from_yaml_simple() {
let v = ev(r#"builtins.sui.fromYAML "x: 1\ny: hello\n""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("x"), Some(&Value::Int(1)));
assert_eq!(
a.get("y").and_then(|v| if let Value::String(ns) = v { Some(ns.chars.to_string()) } else { None }),
Some("hello".to_string()),
);
} else { panic!(); }
}
#[test]
fn sui_ext_from_yaml_invalid_errors() {
let result = eval(r#"builtins.sui.fromYAML "this is :\n: not valid: : :: ::""#);
assert!(result.is_err());
}
#[test]
fn sui_ext_to_yaml_round_trip() {
let v = ev(r#"builtins.sui.fromYAML (builtins.sui.toYAML { a = 1; b = "two"; })"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(1)));
} else { panic!(); }
}
#[test]
fn sui_ext_from_csv_with_header() {
let v = ev(r#"builtins.sui.fromCSV "name,age\nalice,30\nbob,25" { hasHeader = true; }"#);
if let Value::List(rows) = v {
assert_eq!(rows.len(), 2);
if let Value::Attrs(a) = &rows[0] {
assert_eq!(
a.get("name").and_then(|v| if let Value::String(ns) = v { Some(ns.chars.to_string()) } else { None }),
Some("alice".to_string()),
);
} else { panic!(); }
} else { panic!(); }
}
#[test]
fn sui_ext_from_csv_no_header() {
let v = ev(r#"builtins.sui.fromCSV "a,b\nc,d" { hasHeader = false; }"#);
if let Value::List(rows) = v {
assert_eq!(rows.len(), 2);
if let Value::List(cells) = &rows[0] { assert_eq!(cells.len(), 2); } else { panic!(); }
} else { panic!(); }
}
#[test]
fn sui_ext_from_csv_custom_delimiter() {
let v = ev(r#"builtins.sui.fromCSV "x|y\n1|2" { hasHeader = true; delimiter = "|"; }"#);
if let Value::List(rows) = v {
assert_eq!(rows.len(), 1);
if let Value::Attrs(a) = &rows[0] {
assert_eq!(
a.get("x").and_then(|v| if let Value::String(ns) = v { Some(ns.chars.to_string()) } else { None }),
Some("1".to_string()),
);
} else { panic!(); }
} else { panic!(); }
}
#[test]
fn sui_ext_regex_named_captures_match() {
let v = ev(r#"builtins.sui.regexNamedCaptures "(?P<word>[a-z]+) (?P<num>[0-9]+)" "abc 123""#);
if let Value::Attrs(a) = v {
assert_eq!(
a.get("word").and_then(|v| if let Value::String(ns) = v { Some(ns.chars.to_string()) } else { None }),
Some("abc".to_string()),
);
assert_eq!(
a.get("num").and_then(|v| if let Value::String(ns) = v { Some(ns.chars.to_string()) } else { None }),
Some("123".to_string()),
);
} else { panic!(); }
}
#[test]
fn sui_ext_regex_named_captures_no_match() {
assert_eq!(
ev(r#"builtins.sui.regexNamedCaptures "(?P<x>[0-9]+)" "no digits""#),
Value::Null,
);
}
#[test]
fn sui_ext_regex_named_captures_invalid_pattern_errors() {
let result = eval(r#"builtins.sui.regexNamedCaptures "(unclosed" "subject""#);
assert!(result.is_err());
}
#[test]
fn sui_ext_timestamp_format() {
let v = ev("builtins.sui.timestamp null");
if let Value::String(s) = v {
assert_eq!(s.chars.len(), 20);
assert_eq!(&s.chars[10..11], "T");
assert_eq!(&s.chars[19..20], "Z");
} else { panic!(); }
}
#[test]
fn sui_ext_file_size_known() {
let dir = std::env::temp_dir();
let path = dir.join("sui_ext_file_size_test.bin");
std::fs::write(&path, b"hello world").unwrap();
let expr = format!(r#"builtins.sui.fileSize "{}""#, path.display());
assert_eq!(eval(&expr).unwrap(), Value::Int(11));
std::fs::remove_file(&path).ok();
}
#[test]
fn sui_ext_file_size_missing_errors() {
let result = eval(r#"builtins.sui.fileSize "/nonexistent/sui-file-size-12345""#);
assert!(result.is_err());
}
#[test]
fn sui_ext_file_mtime_returns_int() {
let dir = std::env::temp_dir();
let path = dir.join("sui_ext_file_mtime_test.bin");
std::fs::write(&path, b"x").unwrap();
let expr = format!(r#"builtins.sui.fileMtime "{}""#, path.display());
let v = eval(&expr).unwrap();
if let Value::Int(t) = v { assert!(t > 0); } else { panic!(); }
std::fs::remove_file(&path).ok();
}
#[test]
fn builtins_self_reference_exists() {
assert_eq!(ev("builtins ? builtins"), Value::Bool(true));
}
#[test]
fn builtins_self_reference_has_length() {
let v = ev("builtins.builtins ? typeOf");
assert_eq!(v, Value::Bool(true));
}
#[test]
fn builtins_self_reference_does_not_loop() {
assert_eq!(ev("builtins.builtins ? builtins"), Value::Bool(false));
}
#[test]
fn to_lower_basic() {
assert_eq!(ev(r#"builtins.toLower "HELLO""#), Value::string("hello"));
}
#[test]
fn to_upper_basic() {
assert_eq!(ev(r#"builtins.toUpper "hello""#), Value::string("HELLO"));
}
#[test]
fn to_lower_empty() {
assert_eq!(ev(r#"builtins.toLower """#), Value::string(""));
}
#[test]
fn to_upper_mixed() {
assert_eq!(ev(r#"builtins.toUpper "MiXeD""#), Value::string("MIXED"));
}
#[test]
fn to_lower_already() {
assert_eq!(ev(r#"builtins.toLower "already""#), Value::string("already"));
}
#[test]
fn flake_no_lock_file_stubs_inputs_from_flake_nix() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
inputs.utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, utils }: {
ok = true;
};
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").ok"#);
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Bool(true));
}
#[test]
fn flake_no_lock_file_stub_inputs_have_outpath() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.dep.url = "github:example/dep";
outputs = { self, dep }: {
has_out = dep ? outPath;
};
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let expr = format!(r#"(builtins.getFlake "{flake_path}").has_out"#);
assert_eq!(eval(&expr).unwrap(), Value::Bool(true));
}
#[test]
fn flake_no_lock_file_stubs_appear_in_inputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.alpha.url = "github:example/alpha";
inputs.beta.url = "github:example/beta";
outputs = { self, alpha, beta }: { };
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let has_alpha = format!(r#"(builtins.getFlake "{flake_path}").inputs ? alpha"#);
let has_beta = format!(r#"(builtins.getFlake "{flake_path}").inputs ? beta"#);
assert_eq!(eval(&has_alpha).unwrap(), Value::Bool(true));
assert_eq!(eval(&has_beta).unwrap(), Value::Bool(true));
}
#[test]
fn flake_partial_lock_stubs_missing_inputs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("flake.nix"),
r#"{
inputs.locked-dep = { };
inputs.unlocked-dep.url = "github:example/unlocked";
outputs = { self, locked-dep, unlocked-dep }: {
locked = locked-dep ? narHash;
unlocked-has-out = unlocked-dep ? outPath;
};
}"#,
)
.unwrap();
std::fs::write(
dir.path().join("flake.lock"),
r#"{
"nodes": {
"locked-dep": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-PARTIAL=",
"path": "/var/empty/dep",
"type": "path"
},
"original": { "type": "path", "url": "/var/empty/dep" },
"flake": false
},
"root": { "inputs": { "locked-dep": "locked-dep" } }
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let flake_path = dir.path().to_string_lossy().to_string();
let locked = format!(r#"(builtins.getFlake "{flake_path}").locked"#);
let unlocked = format!(r#"(builtins.getFlake "{flake_path}").unlocked-has-out"#);
assert_eq!(eval(&locked).unwrap(), Value::Bool(true));
assert_eq!(eval(&unlocked).unwrap(), Value::Bool(true));
}
#[test]
fn import_relative_dot_normalized() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("bar.nix"), "42").unwrap();
std::fs::write(tmp.path().join("foo.nix"), "import ./bar.nix").unwrap();
let foo_path = tmp.path().join("foo.nix");
let expr = format!(r#"import {}"#, foo_path.display());
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Int(42));
}
#[test]
fn import_relative_parent_normalized() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("sub")).unwrap();
std::fs::write(tmp.path().join("bar.nix"), "99").unwrap();
std::fs::write(tmp.path().join("sub/foo.nix"), "import ../bar.nix").unwrap();
let foo_path = tmp.path().join("sub/foo.nix");
let expr = format!(r#"import {}"#, foo_path.display());
let result = eval(&expr).unwrap();
assert_eq!(result, Value::Int(99));
}
#[test]
fn evaluate_flake_with_relative_imports() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("lib.nix"), "{ x = 1; }").unwrap();
std::fs::write(
tmp.path().join("flake.nix"),
r#"{
description = "test";
outputs = { self }: { value = (import ./lib.nix).x; };
}"#,
)
.unwrap();
let repo = crate::git::init_repo(tmp.path(), "main").unwrap();
crate::git::commit_all(&repo, "init", "test", "test@test.com").ok();
let result = crate::builtins::evaluate_flake(tmp.path()).unwrap();
let val = crate::builtins::navigate_attrs(&result, &["value"]).unwrap();
assert_eq!(val, Value::Int(1));
}
#[test]
fn evaluate_flake_nested_relative_imports() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("lib")).unwrap();
std::fs::write(tmp.path().join("lib/helper.nix"), "{ y = 2; }").unwrap();
std::fs::write(
tmp.path().join("lib/default.nix"),
"{ x = 1; helper = import ./helper.nix; }",
)
.unwrap();
std::fs::write(
tmp.path().join("flake.nix"),
r#"{
description = "test";
outputs = { self }: let lib = import ./lib; in { value = lib.x + lib.helper.y; };
}"#,
)
.unwrap();
let repo = crate::git::init_repo(tmp.path(), "main").unwrap();
crate::git::commit_all(&repo, "init", "test", "test@test.com").ok();
let result = crate::builtins::evaluate_flake(tmp.path()).unwrap();
let val = crate::builtins::navigate_attrs(&result, &["value"]).unwrap();
assert_eq!(val, Value::Int(3));
}
#[test]
fn normalize_path_removes_dot() {
let p = std::path::Path::new("/a/b/./c");
assert_eq!(crate::path::normalize(p), std::path::PathBuf::from("/a/b/c"));
}
#[test]
fn normalize_path_resolves_parent() {
let p = std::path::Path::new("/a/b/../c");
assert_eq!(crate::path::normalize(p), std::path::PathBuf::from("/a/c"));
}
#[test]
fn normalize_path_complex() {
let p = std::path::Path::new("/a/b/./c/../d/./e/../f");
assert_eq!(crate::path::normalize(p), std::path::PathBuf::from("/a/b/d/f"));
}
#[test]
fn evaluate_flake_depth_limit_triggers() {
let tmp = tempfile::tempdir().unwrap();
let flake_dir = tmp.path().join("deep-flake");
std::fs::create_dir_all(&flake_dir).unwrap();
FLAKE_EVAL_DEPTH.with(|d| *d.borrow_mut() = MAX_FLAKE_EVAL_DEPTH);
let result = evaluate_flake(&flake_dir);
FLAKE_EVAL_DEPTH.with(|d| *d.borrow_mut() = 0);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("recursion limit"),
"expected recursion limit error, got: {msg}"
);
}
#[test]
fn evaluate_flake_depth_counter_resets_on_error() {
let tmp = tempfile::tempdir().unwrap();
let flake_dir = tmp.path().join("no-flake");
std::fs::create_dir_all(&flake_dir).unwrap();
FLAKE_EVAL_DEPTH.with(|d| *d.borrow_mut() = 0);
let _ = evaluate_flake(&flake_dir);
let depth = FLAKE_EVAL_DEPTH.with(|d| *d.borrow());
assert_eq!(depth, 0, "depth counter should reset to 0 after error");
}
#[test]
#[ignore = "TRACKED BUG: an unfetchable flake input yields a NON-EXISTENT store \
path instead of failing. Reproduced 2026-08-11: a lock naming \
github:nonexistent-owner-zzz with a narHash returns \
outPath=/nix/store/8q8wq…-source in 0.01s — no network attempt, and \
`Path::exists()` is false. The store path is derived from the lock's \
narHash without ever fetching, so a broken input reads as a valid \
source and a build proceeds against content that was never \
materialised. NOT a stale test: this assertion is correct and the \
code is wrong. Un-ignore to verify a fix — the assertions below \
already encode the right contract (lazy at eval, erroring at force)."]
fn evaluate_flake_fetch_failure_returns_error() {
let tmp = tempfile::tempdir().unwrap();
let flake_dir = tmp.path();
std::fs::write(
flake_dir.join("flake.nix"),
r#"{ outputs = { self, ... }: { }; }"#,
)
.unwrap();
std::fs::write(
flake_dir.join("flake.lock"),
r#"{
"nodes": {
"root": {
"inputs": { "fake-input": "fake-input" }
},
"fake-input": {
"locked": {
"type": "github",
"owner": "nonexistent-owner-zzz",
"repo": "nonexistent-repo-zzz",
"rev": "0000000000000000000000000000000000000000",
"narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
"original": {
"type": "github",
"owner": "nonexistent-owner-zzz",
"repo": "nonexistent-repo-zzz"
}
}
},
"root": "root",
"version": 7
}"#,
)
.unwrap();
let result = evaluate_flake(flake_dir);
assert!(
result.is_ok(),
"inputs are lazy: evaluating the flake must not fetch anything; got: {result:?}"
);
let flake_path = flake_dir.to_string_lossy().to_string();
let forced = eval(&format!(
r#"(builtins.getFlake "{flake_path}").inputs.fake-input.outPath"#
));
assert!(
forced.is_err(),
"forcing an unfetchable input must fail, never yield a placeholder \
path; got: {forced:?}"
);
let msg = forced.unwrap_err().to_string();
assert!(
msg.contains("fetch flake input"),
"expected fetch error message, got: {msg}"
);
}
#[test]
fn flake_lazy_input_doesnt_fail_eagerly() {
let tmp = tempfile::tempdir().unwrap();
let dep = tmp.path().join("dep");
std::fs::create_dir_all(&dep).unwrap();
std::fs::write(
dep.join("flake.nix"),
r#"{
description = "broken dep";
outputs = { self }: { broken = builtins.abort "should not be forced"; working = 1; };
}"#,
)
.unwrap();
let dep_repo = crate::git::init_repo(&dep, "main").unwrap();
crate::git::commit_all(&dep_repo, "init", "test", "test@test.com").ok();
let main = tmp.path().join("main");
std::fs::create_dir_all(&main).unwrap();
std::fs::write(
main.join("flake.nix"),
&format!(
r#"{{
description = "main";
inputs.dep.url = "path:{dep}";
outputs = {{ self, dep }}: {{ value = dep.outPath; }};
}}"#,
dep = dep.display()
),
)
.unwrap();
std::fs::write(
main.join("flake.lock"),
&format!(
r#"{{
"nodes": {{
"root": {{
"inputs": {{ "dep": "dep" }}
}},
"dep": {{
"locked": {{
"type": "path",
"path": "{dep}"
}},
"original": {{
"type": "path",
"path": "{dep}"
}}
}}
}},
"root": "root",
"version": 7
}}"#,
dep = dep.display()
),
)
.unwrap();
let main_repo = crate::git::init_repo(&main, "main").unwrap();
crate::git::commit_all(&main_repo, "init", "test", "test@test.com").ok();
let result = evaluate_flake(&main);
assert!(
result.is_ok(),
"lazy input should not fail eagerly: {:?}",
result.err()
);
}
#[test]
fn flake_lazy_input_outputs_forced_on_access() {
let tmp = tempfile::tempdir().unwrap();
let dep = tmp.path().join("dep");
std::fs::create_dir_all(&dep).unwrap();
std::fs::write(
dep.join("flake.nix"),
r#"{
description = "good dep";
outputs = { self }: { answer = 42; };
}"#,
)
.unwrap();
let dep_repo = crate::git::init_repo(&dep, "main").unwrap();
crate::git::commit_all(&dep_repo, "init", "test", "test@test.com").ok();
let main = tmp.path().join("main");
std::fs::create_dir_all(&main).unwrap();
std::fs::write(
main.join("flake.nix"),
&format!(
r#"{{
description = "main";
inputs.dep.url = "path:{dep}";
outputs = {{ self, dep }}: {{ value = dep.answer; }};
}}"#,
dep = dep.display()
),
)
.unwrap();
std::fs::write(
main.join("flake.lock"),
&format!(
r#"{{
"nodes": {{
"root": {{
"inputs": {{ "dep": "dep" }}
}},
"dep": {{
"locked": {{
"type": "path",
"path": "{dep}"
}},
"original": {{
"type": "path",
"path": "{dep}"
}}
}}
}},
"root": "root",
"version": 7
}}"#,
dep = dep.display()
),
)
.unwrap();
let main_repo = crate::git::init_repo(&main, "main").unwrap();
crate::git::commit_all(&main_repo, "init", "test", "test@test.com").ok();
let result = evaluate_flake(&main).unwrap();
let val = crate::builtins::navigate_attrs(&result, &["value"]).unwrap();
assert_eq!(val, Value::Int(42));
}
#[test]
fn flake_git_input_outputs_read_from_cache_not_store_path() {
let Some(dep) = make_local_git_repo() else {
eprintln!("skip: git not available");
return;
};
std::fs::write(
dep.join("flake.nix"),
r#"{
description = "git dep";
outputs = { self }: { answer = 42; darwinModules.default = { ... }: {}; };
}"#,
)
.unwrap();
let dep_repo = gix::open(&dep).unwrap();
crate::git::commit_all(&dep_repo, "add flake", "sui-test", "test@sui.local").ok();
let dep_rev = crate::git::head_rev(&dep).unwrap();
let dep_url = format!("file://{}", dep.display());
let main = tempfile::tempdir().unwrap();
std::fs::write(
main.path().join("flake.nix"),
r#"{
description = "main";
inputs.dep = { };
outputs = { self, dep }: {
value = dep.answer;
hasDarwin = dep ? darwinModules;
};
}"#,
)
.unwrap();
std::fs::write(
main.path().join("flake.lock"),
format!(
r#"{{
"nodes": {{
"root": {{ "inputs": {{ "dep": "dep" }} }},
"dep": {{
"locked": {{
"type": "git",
"url": "{dep_url}",
"rev": "{dep_rev}",
"narHash": "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}},
"original": {{ "type": "git", "url": "{dep_url}" }}
}}
}},
"root": "root",
"version": 7
}}"#
),
)
.unwrap();
let result = match evaluate_flake(main.path()) {
Ok(r) => r,
Err(e) => {
eprintln!("skip: git fetch unavailable: {e:?}");
let _ = std::fs::remove_dir_all(&dep);
return;
}
};
let val = crate::builtins::navigate_attrs(&result, &["value"]).unwrap();
assert_eq!(val, Value::Int(42), "dep flake output must survive the store-path/cache split");
let has_darwin = crate::builtins::navigate_attrs(&result, &["hasDarwin"]).unwrap();
assert_eq!(has_darwin, Value::Bool(true), "dep.darwinModules must be reachable (the marquee root)");
let _ = std::fs::remove_dir_all(&dep);
}
#[test]
fn native_thunk_forces_correctly() {
use crate::value::Thunk;
let thunk = Thunk::new_native(|| Ok(Value::Int(99)));
assert!(!thunk.is_evaluated());
let val = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
assert_eq!(val, Value::Int(99));
assert!(thunk.is_evaluated());
}
#[test]
fn native_thunk_error_re_raises_never_null() {
use crate::value::Thunk;
let thunk = Thunk::new_native(|| {
Err(crate::value::EvalError::Throw("test error".into()))
});
let r1 = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(r1.is_err(), "first force must surface the error");
let r2 = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(
r2.is_err(),
"re-force of a failed Native thunk must re-raise, got {r2:?}"
);
assert_ne!(
r2.ok(),
Some(Value::Null),
"re-force must NOT silently poison to Null (the darwinModules root)"
);
}
#[test]
fn import_cache_returns_same_value() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("cached.nix");
std::fs::write(&path, "42").unwrap();
super::clear_import_cache();
let v1 = eval(&format!("import {}", path.display())).unwrap();
let v2 = eval(&format!("import {}", path.display())).unwrap();
assert_eq!(v1, v2);
assert_eq!(v1, Value::Int(42));
}
#[test]
fn import_cache_function_reused() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("func.nix");
std::fs::write(&path, "x: x + 1").unwrap();
super::clear_import_cache();
let v1 = eval(&format!("(import {}) 1", path.display())).unwrap();
let v2 = eval(&format!("(import {}) 2", path.display())).unwrap();
assert_eq!(v1, Value::Int(2));
assert_eq!(v2, Value::Int(3));
}
#[test]
fn import_cache_survives_across_calls() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("lib.nix");
std::fs::write(&path, "{ x = 1; }").unwrap();
super::clear_import_cache();
let _ = eval(&format!("import {}", path.display())).unwrap();
std::fs::write(&path, "{ x = 2; }").unwrap();
let v = eval(&format!("(import {}).x", path.display())).unwrap();
assert_eq!(v, Value::Int(1)); }
#[test]
fn import_cache_different_paths_different_entries() {
let tmp = tempfile::tempdir().unwrap();
let path_a = tmp.path().join("a.nix");
let path_b = tmp.path().join("b.nix");
std::fs::write(&path_a, "10").unwrap();
std::fs::write(&path_b, "20").unwrap();
super::clear_import_cache();
let va = eval(&format!("import {}", path_a.display())).unwrap();
let vb = eval(&format!("import {}", path_b.display())).unwrap();
assert_eq!(va, Value::Int(10));
assert_eq!(vb, Value::Int(20));
}
#[test]
fn import_cache_attrs_cached() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("attrs.nix");
std::fs::write(&path, "{ a = 1; b = 2; }").unwrap();
super::clear_import_cache();
let v1 = eval(&format!("(import {}).a", path.display())).unwrap();
let v2 = eval(&format!("(import {}).b", path.display())).unwrap();
assert_eq!(v1, Value::Int(1));
assert_eq!(v2, Value::Int(2));
}
#[test]
fn bridge_typeof_int() {
let result = super::call_builtin_by_name("typeOf", &[Value::Int(1)]).unwrap();
assert_eq!(result, Value::string("int"));
}
#[test]
fn bridge_typeof_bool() {
let result = super::call_builtin_by_name("typeOf", &[Value::Bool(true)]).unwrap();
assert_eq!(result, Value::string("bool"));
}
#[test]
fn bridge_length() {
let result = super::call_builtin_by_name(
"length",
&[Value::list(vec![Value::Int(1), Value::Int(2)])],
)
.unwrap();
assert_eq!(result, Value::Int(2));
}
#[test]
fn bridge_head() {
let result =
super::call_builtin_by_name("head", &[Value::list(vec![Value::Int(1)])]).unwrap();
assert_eq!(result, Value::Int(1));
}
#[test]
fn bridge_add() {
let result =
super::call_builtin_by_name("add", &[Value::Int(1), Value::Int(2)]).unwrap();
assert_eq!(result, Value::Int(3));
}
#[test]
fn bridge_string_length() {
let result =
super::call_builtin_by_name("stringLength", &[Value::string("hello")]).unwrap();
assert_eq!(result, Value::Int(5));
}
#[test]
fn bridge_unknown_builtin_returns_error() {
let result = super::call_builtin_by_name("unknown_builtin_xyz", &[Value::Null]);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("unknown builtin"));
}
#[test]
fn lazy_unused_function_arg_not_forced() {
assert_eq!(
ev(r#"({ used, unused }: used) { used = "ok"; unused = throw "forced"; }"#),
Value::string("ok"),
);
}
#[test]
fn lazy_intersect_attrs_does_not_force_values() {
assert_eq!(
ev(r#"builtins.attrNames (builtins.intersectAttrs { a = true; } { a = throw "forced"; b = 1; })"#),
ev(r#"[ "a" ]"#),
);
}
#[test]
fn lazy_implication_short_circuits() {
assert_eq!(ev(r#"false -> (throw "forced")"#), Value::Bool(true));
}
#[test]
fn lazy_make_overridable_override_keeps_unused_origarg_lazy() {
assert_eq!(
ev(r#"let makeOverridable = f: origArgs: (f origArgs) // { override = new: makeOverridable f (origArgs // new); };
fn = { enableCrypt ? true, libxcrypt ? null }: { name = "p"; use = if enableCrypt then libxcrypt else null; };
p = makeOverridable fn { enableCrypt = true; libxcrypt = throw "forced"; };
in (p.override { enableCrypt = false; }).name"#),
Value::string("p"),
);
}
#[test]
fn builtins_path_returns_string_not_path() {
let dir = std::env::temp_dir().join("sui-builtins-path-test");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("probe.patch");
std::fs::write(&file, b"probe contents\n").unwrap();
let expr = format!(
r#"builtins.typeOf (builtins.path {{ name = "probe.patch"; path = {}; }})"#,
file.display()
);
assert_eq!(ev(&expr), Value::string("string"));
}
#[test]
fn builtins_path_does_not_double_copy_in_derivation_env() {
let dir = std::env::temp_dir().join("sui-builtins-path-test2");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("probe2.patch");
std::fs::write(&file, b"probe2 contents\n").unwrap();
let expr = format!(
r#"let p = builtins.path {{ name = "probe2.patch"; path = {}; }};
in toString p"#,
file.display()
);
let s = ev(&expr).as_string().unwrap().to_string();
let base = s.rsplit('/').next().unwrap();
assert!(base.ends_with("-probe2.patch"), "unexpected store name: {base}");
let hash = base.split('-').next().unwrap();
assert_eq!(hash.len(), 32, "store hash must be 32 chars, got {base}");
assert!(!base.contains("-probe2.patch-"), "doubled name: {base}");
}
#[test]
fn builtins_path_filter_changes_store_path() {
let dir = std::env::temp_dir().join("sui-builtins-path-filter");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("keep.txt"), b"keep\n").unwrap();
std::fs::write(dir.join("drop.txt"), b"drop\n").unwrap();
let unfiltered = format!(
r#"toString (builtins.path {{ name = "src"; path = {}; }})"#,
dir.display()
);
let filtered = format!(
r#"toString (builtins.path {{
name = "src";
path = {};
filter = p: t: (builtins.baseNameOf p) != "drop.txt";
}})"#,
dir.display()
);
let u = ev(&unfiltered).as_string().unwrap().to_string();
let f = ev(&filtered).as_string().unwrap().to_string();
assert_ne!(u, f, "filtered path must differ from unfiltered");
for p in [&u, &f] {
let base = p.rsplit('/').next().unwrap();
assert!(base.ends_with("-src"), "unexpected name: {base}");
assert_eq!(base.split('-').next().unwrap().len(), 32);
}
let keep_all = format!(
r#"toString (builtins.path {{ name = "src"; path = {}; filter = p: t: true; }})"#,
dir.display()
);
assert_eq!(ev(&keep_all).as_string().unwrap().to_string(), u);
}