feat: implement SCC analysis; refactor test; rename js helper functions

This commit is contained in:
2026-01-10 17:24:10 +08:00
parent 36ccc735f9
commit 1adb7a24a9
28 changed files with 2445 additions and 870 deletions

View File

@@ -0,0 +1,65 @@
mod utils;
use nix_js::value::Value;
use utils::eval;
#[test]
fn arithmetic() {
assert_eq!(eval("1 + 1"), Value::Int(2));
}
#[test]
fn simple_function_application() {
assert_eq!(eval("(x: x) 1"), Value::Int(1));
}
#[test]
fn curried_function() {
assert_eq!(eval("(x: y: x - y) 2 1"), Value::Int(1));
}
#[test]
fn rec_attrset() {
assert_eq!(eval("rec { b = a; a = 1; }.b"), Value::Int(1));
}
#[test]
fn let_binding() {
assert_eq!(eval("let b = a; a = 1; in b"), Value::Int(1));
}
#[test]
fn fibonacci() {
assert_eq!(
eval(
"let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30"
),
Value::Int(832040)
);
}
#[test]
fn fixed_point_combinator() {
assert_eq!(
eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y"),
Value::Int(2)
);
}
#[test]
fn conditional_true() {
assert_eq!(eval("if true then 1 else 0"), Value::Int(1));
}
#[test]
fn conditional_false() {
assert_eq!(eval("if false then 1 else 0"), Value::Int(0));
}
#[test]
fn nested_let() {
assert_eq!(
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
Value::Int(3)
);
}

149
nix-js/tests/builtins.rs Normal file
View File

@@ -0,0 +1,149 @@
mod utils;
use nix_js::value::{List, Value};
use utils::eval;
#[test]
fn builtins_accessible() {
let result = eval("builtins");
assert!(matches!(result, Value::AttrSet(_)));
}
#[test]
fn builtins_self_reference() {
let result = eval("builtins.builtins");
assert!(matches!(result, Value::AttrSet(_)));
}
#[test]
fn builtins_add() {
assert_eq!(eval("builtins.add 1 2"), Value::Int(3));
}
#[test]
fn builtins_length() {
assert_eq!(eval("builtins.length [1 2 3]"), Value::Int(3));
}
#[test]
fn builtins_map() {
assert_eq!(
eval("builtins.map (x: x * 2) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)]))
);
}
#[test]
fn builtins_filter() {
assert_eq!(
eval("builtins.filter (x: x > 1) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test]
fn builtins_attrnames() {
let result = eval("builtins.attrNames { a = 1; b = 2; }");
assert!(matches!(result, Value::List(_)));
if let Value::List(list) = result {
assert_eq!(format!("{:?}", list).matches(',').count() + 1, 2);
}
}
#[test]
fn builtins_head() {
assert_eq!(eval("builtins.head [1 2 3]"), Value::Int(1));
}
#[test]
fn builtins_tail() {
assert_eq!(
eval("builtins.tail [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test]
fn builtins_in_let() {
assert_eq!(eval("let b = builtins; in b.add 5 3"), Value::Int(8));
}
#[test]
fn builtins_in_with() {
assert_eq!(eval("with builtins; add 10 20"), Value::Int(30));
}
#[test]
fn builtins_nested_calls() {
assert_eq!(
eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)"),
Value::Int(11)
);
}
#[test]
fn builtins_is_list() {
assert_eq!(eval("builtins.isList [1 2 3]"), Value::Bool(true));
}
#[test]
fn builtins_is_attrs() {
assert_eq!(eval("builtins.isAttrs { a = 1; }"), Value::Bool(true));
}
#[test]
fn builtins_is_function() {
assert_eq!(eval("builtins.isFunction (x: x)"), Value::Bool(true));
}
#[test]
fn builtins_is_null() {
assert_eq!(eval("builtins.isNull null"), Value::Bool(true));
}
#[test]
fn builtins_is_bool() {
assert_eq!(eval("builtins.isBool true"), Value::Bool(true));
}
#[test]
fn builtins_shadowing() {
assert_eq!(
eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3"),
Value::Int(2)
);
}
#[test]
fn builtins_lazy_evaluation() {
let result = eval("builtins.builtins.builtins.add 1 1");
assert_eq!(result, Value::Int(2));
}
#[test]
fn builtins_foldl() {
assert_eq!(
eval("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5]"),
Value::Int(15)
);
}
#[test]
fn builtins_elem() {
assert_eq!(eval("builtins.elem 2 [1 2 3]"), Value::Bool(true));
assert_eq!(eval("builtins.elem 5 [1 2 3]"), Value::Bool(false));
}
#[test]
fn builtins_concat_lists() {
assert_eq!(
eval("builtins.concatLists [[1 2] [3 4] [5]]"),
Value::List(List::new(vec![
Value::Int(1),
Value::Int(2),
Value::Int(3),
Value::Int(4),
Value::Int(5)
]))
);
}

View File

@@ -0,0 +1,75 @@
mod utils;
use nix_js::value::{List, Value};
use utils::eval;
#[test]
fn true_literal() {
assert_eq!(eval("true"), Value::Bool(true));
}
#[test]
fn false_literal() {
assert_eq!(eval("false"), Value::Bool(false));
}
#[test]
fn null_literal() {
assert_eq!(eval("null"), Value::Null);
}
#[test]
fn map_function() {
assert_eq!(
eval("map (x: x * 2) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)]))
);
}
#[test]
fn is_null_function() {
assert_eq!(eval("isNull null"), Value::Bool(true));
assert_eq!(eval("isNull 5"), Value::Bool(false));
}
#[test]
fn shadow_true() {
assert_eq!(eval("let true = false; in true"), Value::Bool(false));
}
#[test]
fn shadow_map() {
assert_eq!(eval("let map = x: y: x; in map 1 2"), Value::Int(1));
}
#[test]
fn mixed_usage() {
assert_eq!(
eval("if true then map (x: x + 1) [1 2] else []"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test]
fn in_let_bindings() {
assert_eq!(
eval("let x = true; y = false; in x && y"),
Value::Bool(false)
);
}
#[test]
fn shadow_in_function() {
assert_eq!(eval("(true: true) false"), Value::Bool(false));
}
#[test]
fn throw_function() {
let result = utils::eval_result("throw \"error message\"");
assert!(result.is_err());
}
#[test]
fn to_string_function() {
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
}

124
nix-js/tests/functions.rs Normal file
View File

@@ -0,0 +1,124 @@
mod utils;
use nix_js::value::Value;
use utils::{eval, eval_result};
#[test]
fn required_parameters() {
assert_eq!(eval("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test]
fn missing_required_parameter() {
let result = eval_result("({ a, b }: a + b) { a = 1; }");
assert!(result.is_err());
}
#[test]
fn all_required_parameters_present() {
assert_eq!(
eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }"),
Value::Int(6)
);
}
#[test]
fn reject_unexpected_arguments() {
let result = eval_result("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
assert!(result.is_err());
}
#[test]
fn ellipsis_accepts_extra_arguments() {
assert_eq!(
eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }"),
Value::Int(3)
);
}
#[test]
fn default_parameters() {
assert_eq!(eval("({ a, b ? 5 }: a + b) { a = 1; }"), Value::Int(6));
}
#[test]
fn override_default_parameter() {
assert_eq!(
eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }"),
Value::Int(11)
);
}
#[test]
fn at_pattern_alias() {
assert_eq!(
eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }"),
Value::Int(3)
);
}
#[test]
fn simple_parameter_no_validation() {
assert_eq!(eval("(x: x.a + x.b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test]
fn simple_parameter_accepts_any_argument() {
assert_eq!(eval("(x: x) 42"), Value::Int(42));
}
#[test]
fn nested_function_parameters() {
assert_eq!(
eval("({ a }: { b }: a + b) { a = 5; } { b = 3; }"),
Value::Int(8)
);
}
#[test]
fn pattern_param_simple_reference_in_default() {
assert_eq!(
eval("({ a, b ? a }: b) { a = 10; }"),
Value::Int(10)
);
}
#[test]
fn pattern_param_multiple_references_in_default() {
assert_eq!(
eval("({ a, b ? a + 5, c ? 1 }: b + c) { a = 10; }"),
Value::Int(16)
);
}
#[test]
fn pattern_param_mutual_reference() {
assert_eq!(
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; }"),
Value::Int(6)
);
}
#[test]
fn pattern_param_override_mutual_reference() {
assert_eq!(
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; c = 10; }"),
Value::Int(11)
);
}
#[test]
fn pattern_param_reference_list() {
assert_eq!(
eval("({ a, b ? [ a 2 ] }: builtins.elemAt b 0) { a = 42; }"),
Value::Int(42)
);
}
#[test]
fn pattern_param_alias_in_default() {
assert_eq!(
eval("(args@{ a, b ? args.a + 10 }: b) { a = 5; }"),
Value::Int(15)
);
}

View File

@@ -0,0 +1,103 @@
mod utils;
use nix_js::context::Context;
use nix_js::value::Value;
#[test]
fn import_absolute_path() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let lib_path = temp_dir.path().join("nix_test_lib.nix");
std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap();
let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(8));
}
#[test]
fn import_nested() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let lib_path = temp_dir.path().join("lib.nix");
std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap();
let main_path = temp_dir.path().join("main.nix");
let main_content = format!(
r#"let lib = import {}; in {{ result = lib.add 10 20; }}"#,
lib_path.display()
);
std::fs::write(&main_path, main_content).unwrap();
let expr = format!(r#"(import "{}").result"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(30));
}
#[test]
fn import_relative_path() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let subdir = temp_dir.path().join("subdir");
std::fs::create_dir_all(&subdir).unwrap();
let lib_path = temp_dir.path().join("lib.nix");
std::fs::write(&lib_path, "{ multiply = a: b: a * b; }").unwrap();
let helper_path = subdir.join("helper.nix");
std::fs::write(&helper_path, "{ subtract = a: b: a - b; }").unwrap();
let main_path = temp_dir.path().join("main.nix");
let main_content = r#"
let
lib = import ./lib.nix;
helper = import ./subdir/helper.nix;
in {
result1 = lib.multiply 3 4;
result2 = helper.subtract 10 3;
}
"#;
std::fs::write(&main_path, main_content).unwrap();
let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(12));
let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(7));
}
#[test]
fn import_returns_function() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let func_path = temp_dir.path().join("nix_test_func.nix");
std::fs::write(&func_path, "x: x * 2").unwrap();
let expr = format!(r#"(import "{}") 5"#, func_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(10));
}
#[test]
fn import_with_complex_dependency_graph() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let utils_path = temp_dir.path().join("utils.nix");
std::fs::write(&utils_path, "{ double = x: x * 2; }").unwrap();
let math_path = temp_dir.path().join("math.nix");
let math_content =
r#"let utils = import ./utils.nix; in { triple = x: x + utils.double x; }"#;
std::fs::write(&math_path, math_content).unwrap();
let main_path = temp_dir.path().join("main.nix");
let main_content = r#"let math = import ./math.nix; in math.triple 5"#;
std::fs::write(&main_path, main_content).unwrap();
let expr = format!(r#"import "{}""#, main_path.display());
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(15));
}

View File

@@ -0,0 +1,139 @@
mod utils;
use nix_js::value::Value;
use utils::eval;
#[test]
fn large_i64_max() {
assert_eq!(eval("9223372036854775807"), Value::Int(9223372036854775807));
}
#[test]
fn large_i64_negative() {
assert_eq!(
eval("-9223372036854775807"),
Value::Int(-9223372036854775807)
);
}
#[test]
fn large_number_arithmetic() {
assert_eq!(
eval("5000000000000000000 + 3000000000000000000"),
Value::Int(8000000000000000000i64)
);
}
#[test]
fn is_int_with_int() {
assert_eq!(eval("builtins.isInt 42"), Value::Bool(true));
}
#[test]
fn is_int_with_float() {
assert_eq!(eval("builtins.isInt 42.0"), Value::Bool(false));
}
#[test]
fn is_float_with_int() {
assert_eq!(eval("builtins.isFloat 42"), Value::Bool(false));
}
#[test]
fn is_float_with_float() {
assert_eq!(eval("builtins.isFloat 42.5"), Value::Bool(true));
assert_eq!(eval("builtins.isFloat 1.0"), Value::Bool(true));
}
#[test]
fn typeof_int() {
assert_eq!(eval("builtins.typeOf 1"), Value::String("int".to_string()));
}
#[test]
fn typeof_float() {
assert_eq!(
eval("builtins.typeOf 1.0"),
Value::String("float".to_string())
);
assert_eq!(
eval("builtins.typeOf 3.14"),
Value::String("float".to_string())
);
}
#[test]
fn int_literal() {
assert_eq!(eval("1"), Value::Int(1));
}
#[test]
fn float_literal() {
assert_eq!(eval("1."), Value::Float(1.));
}
#[test]
fn int_plus_int() {
assert_eq!(
eval("builtins.typeOf (1 + 2)"),
Value::String("int".to_string())
);
}
#[test]
fn int_plus_float() {
assert_eq!(
eval("builtins.typeOf (1 + 2.0)"),
Value::String("float".to_string())
);
}
#[test]
fn int_times_int() {
assert_eq!(
eval("builtins.typeOf (3 * 4)"),
Value::String("int".to_string())
);
}
#[test]
fn int_times_float() {
assert_eq!(
eval("builtins.typeOf (3 * 4.0)"),
Value::String("float".to_string())
);
}
#[test]
fn integer_division() {
assert_eq!(eval("5 / 2"), Value::Int(2));
assert_eq!(eval("7 / 3"), Value::Int(2));
assert_eq!(eval("10 / 3"), Value::Int(3));
}
#[test]
fn float_division() {
assert_eq!(eval("5 / 2.0"), Value::Float(2.5));
assert_eq!(eval("7.0 / 2"), Value::Float(3.5));
}
#[test]
fn negative_integer_division() {
assert_eq!(eval("(-7) / 3"), Value::Int(-2));
}
#[test]
fn builtin_add_with_large_numbers() {
assert_eq!(
eval("builtins.add 5000000000000000000 3000000000000000000"),
Value::Int(8000000000000000000i64)
);
}
#[test]
fn builtin_mul_with_large_numbers() {
assert_eq!(
eval("builtins.mul 1000000000 1000000000"),
Value::Int(1000000000000000000i64)
);
}

101
nix-js/tests/operators.rs Normal file
View File

@@ -0,0 +1,101 @@
mod utils;
use nix_js::value::{AttrSet, List, Symbol, Value};
use std::collections::BTreeMap;
use utils::eval;
#[test]
fn addition() {
assert_eq!(eval("1 + 1"), Value::Int(2));
}
#[test]
fn subtraction() {
assert_eq!(eval("2 - 1"), Value::Int(1));
}
#[test]
fn multiplication() {
assert_eq!(eval("1. * 1"), Value::Float(1.));
}
#[test]
fn division() {
assert_eq!(eval("1 / 1."), Value::Float(1.));
}
#[test]
fn equality() {
assert_eq!(eval("1 == 1"), Value::Bool(true));
}
#[test]
fn inequality() {
assert_eq!(eval("1 != 1"), Value::Bool(false));
}
#[test]
fn less_than() {
assert_eq!(eval("2 < 1"), Value::Bool(false));
}
#[test]
fn greater_than() {
assert_eq!(eval("2 > 1"), Value::Bool(true));
}
#[test]
fn less_than_or_equal() {
assert_eq!(eval("1 <= 1"), Value::Bool(true));
}
#[test]
fn greater_than_or_equal() {
assert_eq!(eval("1 >= 1"), Value::Bool(true));
}
#[test]
fn logical_or_short_circuit() {
assert_eq!(eval("true || (1 / 0)"), Value::Bool(true));
}
#[test]
fn logical_and() {
assert_eq!(eval("true && 1 == 0"), Value::Bool(false));
}
#[test]
fn list_concatenation() {
assert_eq!(
eval("[ 1 2 3 ] ++ [ 4 5 6 ]"),
Value::List(List::new((1..=6).map(Value::Int).collect()))
);
}
#[test]
fn attrset_update() {
assert_eq!(
eval("{ a.b = 1; b = 2; } // { a.c = 2; }"),
Value::AttrSet(AttrSet::new(BTreeMap::from([
(
Symbol::from("a"),
Value::AttrSet(AttrSet::new(BTreeMap::from([(
Symbol::from("c"),
Value::Int(2),
)]))),
),
(Symbol::from("b"), Value::Int(2)),
])))
);
}
#[test]
fn unary_negation() {
assert_eq!(eval("-5"), Value::Int(-5));
}
#[test]
fn logical_not() {
assert_eq!(eval("!true"), Value::Bool(false));
assert_eq!(eval("!false"), Value::Bool(true));
}

View File

@@ -0,0 +1,120 @@
mod utils;
use nix_js::value::Value;
use utils::eval;
#[test]
fn non_recursive_bindings() {
assert_eq!(eval("let x = 1; y = 2; z = x + y; in z"), Value::Int(3));
}
#[test]
fn non_recursive_multiple_bindings() {
assert_eq!(
eval("let a = 10; b = 20; c = 30; d = a + b + c; in d"),
Value::Int(60)
);
}
#[test]
fn recursive_fibonacci() {
assert_eq!(
eval("let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 5"),
Value::Int(8)
);
}
#[test]
fn recursive_factorial() {
assert_eq!(
eval("let factorial = n: if n == 0 then 1 else n * factorial (n - 1); in factorial 5"),
Value::Int(120)
);
}
#[test]
fn mutual_recursion_simple() {
assert_eq!(
eval(
"let f = n: if n == 0 then 0 else g (n - 1); g = n: if n == 0 then 1 else f (n - 1); in f 5"
),
Value::Int(1)
);
}
#[test]
fn mutual_recursion_even_odd() {
assert_eq!(
eval(
"let even = n: if n == 0 then true else odd (n - 1); odd = n: if n == 0 then false else even (n - 1); in even 4"
),
Value::Bool(true)
);
}
#[test]
fn mixed_recursive_and_non_recursive() {
assert_eq!(
eval("let x = 1; f = n: if n == 0 then x else f (n - 1); in f 5"),
Value::Int(1)
);
}
#[test]
fn mixed_with_multiple_non_recursive() {
assert_eq!(
eval(
"let a = 10; b = 20; sum = a + b; countdown = n: if n == 0 then sum else countdown (n - 1); in countdown 3"
),
Value::Int(30)
);
}
#[test]
fn rec_attrset_non_recursive() {
assert_eq!(eval("rec { x = 1; y = 2; z = x + y; }.z"), Value::Int(3));
}
#[test]
fn rec_attrset_recursive() {
assert_eq!(
eval("rec { f = n: if n == 0 then 0 else f (n - 1); }.f 10"),
Value::Int(0)
);
}
#[test]
fn nested_let_non_recursive() {
assert_eq!(
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
Value::Int(3)
);
}
#[test]
fn nested_let_with_recursive() {
assert_eq!(
eval("let f = n: if n == 0 then 0 else f (n - 1); in let g = m: f m; in g 5"),
Value::Int(0)
);
}
#[test]
fn three_way_mutual_recursion() {
assert_eq!(
eval(
"let a = n: if n == 0 then 1 else b (n - 1); b = n: if n == 0 then 2 else c (n - 1); c = n: if n == 0 then 3 else a (n - 1); in a 6"
),
Value::Int(1)
);
}
#[test]
fn complex_mixed_dependencies() {
assert_eq!(
eval(
"let x = 5; y = 10; sum = x + y; fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); result = sum + fib 5; in result"
),
Value::Int(23)
);
}

251
nix-js/tests/to_string.rs Normal file
View File

@@ -0,0 +1,251 @@
mod utils;
use nix_js::value::Value;
use utils::eval;
#[test]
fn string_returns_as_is() {
assert_eq!(eval(r#"toString "hello""#), Value::String("hello".to_string()));
}
#[test]
fn integer_to_string() {
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
assert_eq!(eval("toString (-5)"), Value::String("-5".to_string()));
assert_eq!(eval("toString 0"), Value::String("0".to_string()));
}
#[test]
fn float_to_string() {
assert_eq!(eval("toString 3.14"), Value::String("3.14".to_string()));
assert_eq!(eval("toString 0.0"), Value::String("0".to_string()));
assert_eq!(eval("toString (-2.5)"), Value::String("-2.5".to_string()));
}
#[test]
fn bool_to_string() {
assert_eq!(eval("toString true"), Value::String("1".to_string()));
assert_eq!(eval("toString false"), Value::String("".to_string()));
}
#[test]
fn null_to_string() {
assert_eq!(eval("toString null"), Value::String("".to_string()));
}
#[test]
fn simple_list_to_string() {
assert_eq!(eval("toString [1 2 3]"), Value::String("1 2 3".to_string()));
assert_eq!(
eval(r#"toString ["a" "b" "c"]"#),
Value::String("a b c".to_string())
);
}
#[test]
fn nested_list_flattens() {
assert_eq!(
eval("toString [[1 2] [3 4]]"),
Value::String("1 2 3 4".to_string())
);
assert_eq!(
eval("toString [1 [2 3] 4]"),
Value::String("1 2 3 4".to_string())
);
}
#[test]
fn empty_list_in_list_no_extra_space() {
assert_eq!(eval("toString [1 [] 2]"), Value::String("1 2".to_string()));
assert_eq!(eval("toString [[] 1 2]"), Value::String("1 2".to_string()));
assert_eq!(eval("toString [1 2 []]"), Value::String("1 2 ".to_string()));
}
#[test]
fn list_with_multiple_empty_lists() {
assert_eq!(
eval("toString [1 [] [] 2]"),
Value::String("1 2".to_string())
);
assert_eq!(
eval("toString [[] [] 1]"),
Value::String("1".to_string())
);
}
#[test]
fn list_with_bool_and_null() {
assert_eq!(
eval("toString [true false null]"),
Value::String("1 ".to_string())
);
assert_eq!(
eval("toString [1 true 2 false 3]"),
Value::String("1 1 2 3".to_string())
);
}
#[test]
fn mixed_type_list() {
assert_eq!(
eval(r#"toString [1 "hello" 2.5 true]"#),
Value::String("1 hello 2.5 1".to_string())
);
}
#[test]
fn attrs_with_out_path() {
assert_eq!(
eval(r#"toString { outPath = "/nix/store/foo"; }"#),
Value::String("/nix/store/foo".to_string())
);
}
#[test]
fn attrs_with_to_string_method() {
assert_eq!(
eval(r#"toString { __toString = self: "custom"; }"#),
Value::String("custom".to_string())
);
}
#[test]
fn attrs_to_string_self_reference() {
assert_eq!(
eval(r#"let obj = { x = 42; __toString = self: "x is ${toString self.x}"; }; in toString obj"#),
Value::String("x is 42".to_string())
);
}
#[test]
fn attrs_to_string_priority() {
assert_eq!(
eval(
r#"toString { __toString = self: "custom"; outPath = "/nix/store/foo"; }"#
),
Value::String("custom".to_string())
);
}
#[test]
fn derivation_like_object() {
assert_eq!(
eval(
r#"let drv = { type = "derivation"; outPath = "/nix/store/hash-pkg"; }; in toString drv"#
),
Value::String("/nix/store/hash-pkg".to_string())
);
}
#[test]
fn string_interpolation_with_int() {
assert_eq!(
eval(r#""value: ${toString 42}""#),
Value::String("value: 42".to_string())
);
}
#[test]
fn string_interpolation_with_list() {
assert_eq!(
eval(r#""items: ${toString [1 2 3]}""#),
Value::String("items: 1 2 3".to_string())
);
}
#[test]
fn nested_to_string_calls() {
assert_eq!(
eval(r#"toString (toString 42)"#),
Value::String("42".to_string())
);
}
#[test]
fn to_string_in_let_binding() {
assert_eq!(
eval(r#"let x = toString 42; y = toString 10; in "${x}-${y}""#),
Value::String("42-10".to_string())
);
}
#[test]
fn empty_string() {
assert_eq!(eval(r#"toString """#), Value::String("".to_string()));
}
#[test]
fn empty_list() {
assert_eq!(eval("toString []"), Value::String("".to_string()));
}
#[test]
fn to_string_preserves_spaces_in_strings() {
assert_eq!(
eval(r#"toString "hello world""#),
Value::String("hello world".to_string())
);
}
#[test]
fn list_of_empty_strings() {
assert_eq!(
eval(r#"toString ["" "" ""]"#),
Value::String(" ".to_string())
);
}
#[test]
fn deeply_nested_lists() {
assert_eq!(
eval("toString [[[1] [2]] [[3] [4]]]"),
Value::String("1 2 3 4".to_string())
);
}
#[test]
fn list_with_nested_empty_lists() {
assert_eq!(
eval("toString [1 [[]] 2]"),
Value::String("1 2".to_string())
);
}
#[test]
fn attrs_without_out_path_or_to_string_fails() {
let result = utils::eval_result(r#"toString { foo = "bar"; }"#);
assert!(result.is_err());
}
#[test]
fn function_to_string_fails() {
let result = utils::eval_result("toString (x: x)");
assert!(result.is_err());
}
#[test]
fn to_string_method_must_return_string() {
let result = utils::eval_result(r#"toString { __toString = self: 42; }"#);
assert!(result.is_err());
}
#[test]
fn out_path_can_be_nested() {
assert_eq!(
eval(r#"toString { outPath = { outPath = "/final/path"; }; }"#),
Value::String("/final/path".to_string())
);
}
#[test]
fn list_spacing_matches_nix_behavior() {
assert_eq!(
eval(r#"toString ["a" "b"]"#),
Value::String("a b".to_string())
);
assert_eq!(
eval(r#"toString ["a" ["b" "c"] "d"]"#),
Value::String("a b c d".to_string())
);
}

12
nix-js/tests/utils.rs Normal file
View File

@@ -0,0 +1,12 @@
#![allow(dead_code)]
use nix_js::context::Context;
use nix_js::value::Value;
pub fn eval(expr: &str) -> Value {
Context::new().unwrap().eval_code(expr).unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value, nix_js::error::Error> {
Context::new().unwrap().eval_code(expr)
}