mod common;
use std::path::PathBuf;
fn resolve_nixpkgs_path() -> Option<PathBuf> {
let out = std::process::Command::new("nix-instantiate")
.args(["--eval", "--expr", "<nixpkgs>"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let raw = String::from_utf8_lossy(&out.stdout).trim().to_string();
if raw.is_empty() {
return None;
}
Some(PathBuf::from(raw))
}
fn lib_expr(body: &str) -> Option<String> {
let p = resolve_nixpkgs_path()?;
Some(format!(
"let lib = import {}/lib; in {body}",
p.display()
))
}
fn diff(body: &str) {
if common::skip_if_offline("diff_nixpkgs_lib") {
return;
}
let Some(expr) = lib_expr(body) else {
eprintln!("skip: cannot resolve <nixpkgs> via real nix");
return;
};
common::assert_eq_nix(&expr);
}
#[test]
fn lib_lists_length() {
diff("lib.lists.length [ 1 2 3 ]");
}
#[test]
fn lib_lists_fold() {
diff("lib.lists.fold (a: b: a + b) 0 [ 1 2 3 4 ]");
}
#[test]
fn lib_lists_foldl_prime() {
diff("lib.lists.foldl' (acc: x: acc + x) 0 [ 1 2 3 4 ]");
}
#[test]
fn lib_lists_unique() {
diff("lib.lists.unique [ 1 2 3 2 1 ]");
}
#[test]
fn lib_lists_flatten() {
diff("lib.lists.flatten [ 1 [ 2 [ 3 4 ] ] 5 ]");
}
#[test]
fn lib_strings_concat_strings() {
diff(r#"lib.strings.concatStrings [ "a" "b" "c" ]"#);
}
#[test]
fn lib_strings_split_string() {
diff(r#"lib.strings.splitString "," "a,b,c,d""#);
}
#[test]
fn lib_strings_has_prefix() {
diff(r#"lib.strings.hasPrefix "abc" "abcdef""#);
}
#[test]
fn lib_strings_has_suffix() {
diff(r#"lib.strings.hasSuffix "def" "abcdef""#);
}
#[test]
fn lib_attrsets_filter_attrs() {
diff("lib.attrsets.filterAttrs (n: v: v > 1) { a = 1; b = 2; c = 3; }");
}
#[test]
fn lib_attrsets_map_attrs_prime() {
diff(r#"lib.attrsets.mapAttrs' (n: v: { name = n + "!"; value = v + 1; }) { a = 1; b = 2; }"#);
}
#[test]
fn lib_attrsets_recursive_update() {
diff(
r#"lib.attrsets.recursiveUpdate
{ a = { b = 1; c = 2; }; d = 3; }
{ a = { b = 10; e = 20; }; f = 30; }"#,
);
}
#[test]
fn lib_trivial_pipe() {
diff("lib.trivial.pipe 3 [ (x: x + 1) (x: x * 2) (x: x - 5) ]");
}
#[test]
fn lib_trivial_id() {
diff("lib.trivial.id 42");
}
#[test]
fn lib_trivial_flip() {
diff("(lib.trivial.flip (a: b: [ a b ])) 1 2");
}
#[test]
fn lib_versions_major() {
diff(r#"lib.versions.major "1.2.3""#);
}
#[test]
fn lib_versions_split_version() {
diff(r#"lib.versions.splitVersion "1.2.3""#);
}
#[test]
fn module_dynamic_key_from_sibling_option_resolves() {
diff(
"(lib.evalModules { modules = [ \
({ config, lib, ... }: { \
options.pleme.userName = lib.mkOption { type = lib.types.str; default = \"luis\"; }; \
options.homes = lib.mkOption { type = lib.types.attrsOf lib.types.int; default = {}; }; \
config.homes.${config.pleme.userName} = 7; }) \
({ ... }: { config.pleme.userName = \"drzzln\"; }) \
]; }).config.homes",
);
}
#[test]
fn module_unrelated_select_does_not_force_sibling_dynamic_key() {
diff(
"(lib.evalModules { modules = [ \
({ config, lib, ... }: { \
options.pleme.userName = lib.mkOption { type = lib.types.str; default = \"luis\"; }; \
options.homes = lib.mkOption { type = lib.types.attrsOf lib.types.int; default = {}; }; \
config.homes.${throw \"KEYFORCE\"} = 7; }) \
({ ... }: { config.pleme.userName = \"drzzln\"; }) \
]; }).config.pleme.userName",
);
}
#[test]
fn module_interpolated_string_dynamic_key_stays_lazy() {
diff(
"(lib.evalModules { modules = [ \
({ lib, ... }: { \
options.other.enable = lib.mkOption { type = lib.types.bool; default = true; }; \
options.probe = lib.mkOption { type = lib.types.int; default = 0; }; \
config.probe = 42; }) \
({ config, lib, ... }: \
let d = with config.other; lib.optionalAttrs enable { x = 1; }; \
nm = if d ? x then \"yes\" else \"no\"; \
in { options.environment.etc = lib.mkOption { type = lib.types.attrs; default = {}; }; \
config.environment.etc.\"iwd/${nm}\" = { source = 1; }; }) \
]; }).config.probe",
);
}
#[test]
fn module_dynamic_tail_key_under_colliding_head_stays_lazy() {
diff(
"(lib.evalModules { modules = [ \
({ lib, ... }: { \
options.svc.path = lib.mkOption { type = lib.types.str; default = \"/a/b\"; }; \
options.probe = lib.mkOption { type = lib.types.int; default = 0; }; \
config.probe = 42; }) \
({ config, lib, ... }: { \
options.sd.services = lib.mkOption { type = lib.types.attrs; default = {}; }; \
options.sd.tmpfiles = lib.mkOption { type = lib.types.attrs; default = {}; }; \
config.sd.services.x = { a = 1; }; \
config.sd.tmpfiles.${builtins.dirOf config.svc.path}.d = { m = 1; }; }) \
]; }).config.probe",
);
}
#[test]
fn module_dynamic_tail_collision_preserves_static_sibling() {
diff(
"(lib.evalModules { modules = [ \
({ lib, ... }: { \
options.svc.path = lib.mkOption { type = lib.types.str; default = \"/a/b\"; }; }) \
({ config, lib, ... }: { \
options.sd.services = lib.mkOption { type = lib.types.attrs; default = {}; }; \
options.sd.tmpfiles = lib.mkOption { type = lib.types.attrs; default = {}; }; \
config.sd.services.x = { a = 1; }; \
config.sd.tmpfiles.${builtins.dirOf config.svc.path}.d = { m = 1; }; }) \
]; }).config.sd",
);
}
#[test]
fn module_rename_forwards_definition() {
diff(
"(lib.evalModules { modules = [ \
(lib.mkRenamedOptionModule [ \"old\" \"opt\" ] [ \"new\" \"opt\" ]) \
({ lib, ... }: { options.new.opt = lib.mkOption { type = lib.types.str; default = \"def\"; }; }) \
({ ... }: { old.opt = \"fromold\"; }) \
]; }).config.new.opt",
);
}
#[test]
fn module_rename_into_submodule_unrelated_select() {
diff(
"(lib.evalModules { modules = [ \
(lib.doRename { from = [ \"old\" \"sub\" \"field\" ]; to = [ \"new\" \"sub\" \"field\" ]; \
visible = false; warn = false; use = x: x; }) \
({ lib, ... }: { \
options.new.sub = lib.mkOption { \
type = lib.types.submodule { options.field = lib.mkOption { type = lib.types.str; default = \"d\"; }; }; \
default = {}; }; \
options.probe = lib.mkOption { type = lib.types.str; default = \"PROBE\"; }; }) \
({ ... }: { old.sub.field = \"fromold\"; }) \
]; }).config.probe",
);
}
#[test]
fn module_mkif_config_read_over_attrs_of_submodule() {
diff(
"(lib.evalModules { modules = [ \
({ config, lib, ... }: { \
options.repos = lib.mkOption { \
type = lib.types.attrsOf (lib.types.submodule { options.user = lib.mkOption { type = lib.types.str; default = \"u\"; }; }); \
default = {}; }; \
options.users = lib.mkOption { type = lib.types.attrsOf lib.types.anything; default = {}; }; \
options.probe = lib.mkOption { type = lib.types.str; default = \"PROBE\"; }; \
config = lib.mkIf (config.repos != {}) (with config.repos; { \
users = lib.mkMerge (lib.mapAttrsToList (n: c: { \"${c.user}\" = {}; }) config.repos); }); }) \
]; }).config.probe",
);
}
#[test]
fn module_with_namespace_config_read_stays_lazy() {
diff(
"(lib.evalModules { modules = [ \
({ config, lib, ... }: { \
options.svc.enable = lib.mkOption { type = lib.types.bool; default = false; }; \
options.out = lib.mkOption { type = lib.types.attrsOf lib.types.int; default = {}; }; \
options.probe = lib.mkOption { type = lib.types.str; default = \"PROBE\"; }; \
config.out = lib.mkIf config.svc.enable (with config.svc; { x = 1; }); }) \
]; }).config.probe",
);
}
#[test]
fn module_options_fullset_and_dotted_sibling_deep_merge() {
diff(
"builtins.filter (n: n == \"enable\" || n == \"extra\") \
(builtins.attrNames \
(lib.evalModules { modules = [ \
({ lib, ... }: { \
options.a = { enable = lib.mkOption { type = lib.types.bool; default = false; }; }; \
options.a.extra = lib.mkOption { type = lib.types.int; default = 0; }; }) \
]; }).options.a)",
);
}