use std::sync::Arc;
use sui_spec::ast_evaluator::EvalValue;
use sui_spec::ast_graph::AstGraph;
use sui_spec::module_graph::ModuleGraph;
use sui_spec::module_solver::{
EnvSnapshot, PerModuleEvaluator, SolverState, TreeWalkingEvaluator,
};
fn build_solver_one_module(
source: &str,
) -> SolverState<TreeWalkingEvaluator> {
let ast = Arc::new(AstGraph::from_source(source).expect("parse"));
let g = ModuleGraph::from_ast_graphs(&[("test.nix".to_string(), (*ast).clone())])
.expect("build module graph");
SolverState::new(g, TreeWalkingEvaluator::new(ast))
}
fn env_value(env: &EnvSnapshot, path: &[&str]) -> EvalValue {
let key: Vec<String> = path.iter().map(|s| (*s).to_string()).collect();
let bytes = env
.get(&key)
.unwrap_or_else(|| panic!("no value for path {path:?}"));
serde_json::from_slice::<EvalValue>(bytes)
.or_else(|_| serde_json::from_str(std::str::from_utf8(bytes).unwrap_or("null")))
.unwrap_or_else(|_| panic!("bytes for {path:?} don't deserialize as EvalValue"))
}
#[test]
fn integer_literal_setter_lands_in_env() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.x = 42; }",
);
let order = solver.run(&[]).expect("solver run");
assert!(!order.is_empty(), "at least one setter must fire");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
}
#[test]
fn string_literal_setter_lands_in_env() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.networking.hostName = \"rio\"; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(
env_value(solver.env(), &["networking", "hostName"]),
EvalValue::Str("rio".to_string())
);
}
#[test]
fn arithmetic_setter_evaluates() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.size = 1024 * 8; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["size"]), EvalValue::Int(8192));
}
#[test]
fn list_setter_lands_in_env() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.boot.kernelParams = [ \"a\" \"b\" \"c\" ]; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(
env_value(solver.env(), &["boot", "kernelParams"]),
EvalValue::List(vec![
EvalValue::Str("a".into()),
EvalValue::Str("b".into()),
EvalValue::Str("c".into()),
])
);
}
#[test]
fn dep_chain_propagates_via_solver() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.a = 5; \
config.b = config.a * 2; \
config.c = config.b + 1; \
}",
);
let order = solver.run(&[]).expect("solver run");
assert!(order.len() >= 3);
assert_eq!(env_value(solver.env(), &["a"]), EvalValue::Int(5));
assert_eq!(env_value(solver.env(), &["b"]), EvalValue::Int(10));
assert_eq!(env_value(solver.env(), &["c"]), EvalValue::Int(11));
}
#[test]
fn warm_re_run_after_slice_change_recomputes_dependents() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.a = 5; \
config.b = config.a * 2; \
config.c = config.b + 1; \
}",
);
solver.run(&[]).expect("cold start");
assert_eq!(env_value(solver.env(), &["c"]), EvalValue::Int(11));
{
let order = solver
.run(&[vec!["a".to_string()]])
.expect("warm re-run");
assert!(
!order.is_empty(),
"warm re-run with dirty 'a' must fire at least the readers of a"
);
}
}
#[test]
fn conditional_body_evaluates_via_if_then_else() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.enabled = 1; \
config.choice = if config.enabled == 1 then 100 else 200; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["enabled"]), EvalValue::Int(1));
assert_eq!(env_value(solver.env(), &["choice"]), EvalValue::Int(100));
}
#[test]
fn multi_module_solver_runs_via_per_module_evaluator() {
let ast_a = Arc::new(
AstGraph::from_source("{ config, ... }: { config.shared = 99; }")
.expect("parse a"),
);
let ast_b = Arc::new(
AstGraph::from_source(
"{ config, ... }: { config.derived = config.shared + 1; }",
)
.expect("parse b"),
);
let g = ModuleGraph::from_ast_graphs(&[
("a.nix".to_string(), (*ast_a).clone()),
("b.nix".to_string(), (*ast_b).clone()),
])
.expect("build");
let evaluator = PerModuleEvaluator::from_pairs([
(0u32, ast_a.clone()),
(1u32, ast_b.clone()),
]);
let mut solver = SolverState::new(g, evaluator);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["shared"]), EvalValue::Int(99));
assert_eq!(env_value(solver.env(), &["derived"]), EvalValue::Int(100));
}
#[test]
fn setter_body_with_inline_let_in_evaluates() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.kernelParams = let p = \"amd_pstate=active\"; q = \"iommu=pt\"; in [ p q ]; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(
env_value(solver.env(), &["kernelParams"]),
EvalValue::List(vec![
EvalValue::Str("amd_pstate=active".into()),
EvalValue::Str("iommu=pt".into()),
])
);
}
#[test]
fn mkforce_captured_as_priority_not_wrapper() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.x = mkForce 42; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
let setter = &solver.graph().modules[0].setters[0];
assert_eq!(setter.priority, 50);
}
#[test]
fn mkif_captured_as_condition_not_wrapper() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.enabled = 1; \
config.kernelParams = mkIf (config.enabled == 1) [\"yes\"]; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(
env_value(solver.env(), &["kernelParams"]),
EvalValue::List(vec![EvalValue::Str("yes".into())])
);
let setter = solver
.graph()
.modules[0]
.setters
.iter()
.find(|s| s.assigns_path == vec!["kernelParams"])
.unwrap();
assert!(setter.condition_ast_root.is_some());
}
#[test]
fn nested_mkif_inside_body_yields_builtin_sentinel() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.x = [ (mkIf true 99) ]; }",
);
solver.run(&[]).expect("solver run");
let v = env_value(solver.env(), &["x"]);
match v {
EvalValue::List(items) => {
assert_eq!(items.len(), 1);
match &items[0] {
EvalValue::Builtin { kind, payload } => {
assert_eq!(kind, "mkIf");
assert_eq!(**payload, EvalValue::Int(99));
}
other => panic!("expected Builtin inside list, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn setter_body_with_inline_with_scope() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.x = with { a = 10; b = 32; }; a + b; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
}
#[test]
fn module_level_let_propagates_to_setter_bodies() {
let mut solver = build_solver_one_module(
"{ config, ... }: \
let answer = 42; in { \
config.x = answer; \
config.y = answer * 2; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
assert_eq!(env_value(solver.env(), &["y"]), EvalValue::Int(84));
}
#[test]
fn module_level_with_propagates_attrset_attrs_as_idents() {
let mut solver = build_solver_one_module(
"{ config, ... }: \
with { localHelper = 7; otherHelper = 8; }; { \
config.x = localHelper + otherHelper; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(15));
}
#[test]
fn nested_let_and_with_compose() {
let mut solver = build_solver_one_module(
"{ config, ... }: \
let base = 100; in \
with { multiplier = 2; }; \
let bonus = 5; in { \
config.total = base * multiplier + bonus; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["total"]), EvalValue::Int(205));
}
#[test]
fn module_level_let_binding_referencing_config_resolves() {
let mut solver = build_solver_one_module(
"{ config, ... }: { \
config.services.atticd.enable = 1; \
config.services.atticd.foo = 42; \
config.x = config.services.atticd.foo; \
}",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
}
#[test]
fn closure_application_inside_body_evaluates() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.x = (n: n + 1) 41; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(env_value(solver.env(), &["x"]), EvalValue::Int(42));
}
#[test]
fn attrset_construction_decomposes_into_per_leaf_setters() {
let mut solver = build_solver_one_module(
"{ config, ... }: { config.services.atticd = { enable = 1; port = 8080; }; }",
);
solver.run(&[]).expect("solver run");
assert_eq!(
env_value(solver.env(), &["services", "atticd", "enable"]),
EvalValue::Int(1)
);
assert_eq!(
env_value(solver.env(), &["services", "atticd", "port"]),
EvalValue::Int(8080)
);
}