diff --git a/implants/lib/eldritch/eldritch-core/tests/analysis_extended.rs b/implants/lib/eldritch/eldritch-core/tests/analysis_extended.rs new file mode 100644 index 000000000..2a5fb3b6f --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/analysis_extended.rs @@ -0,0 +1,166 @@ +extern crate alloc; + +use eldritch_core::{ExprKind, Lexer, Parser, Stmt, analysis::Node, analysis::find_node_at_offset}; + +fn parse(source: &str) -> Vec { + let mut lexer = Lexer::new(source.to_string()); + let tokens = lexer.scan_tokens(); + let mut parser = Parser::new(tokens); + let (ast, _errors) = parser.parse(); + ast +} + +#[test] +fn test_find_in_assignment_annot() { + let source = "x: int = 1"; + let ast = parse(source); + // x (0..1), : (1..2), int (3..6), = (7..8), 1 (9..10) + let node = find_node_at_offset(&ast, 4).unwrap(); + match node { + Node::Expr(e) => match &e.kind { + ExprKind::Identifier(s) => assert_eq!(s, "int"), + _ => panic!("Expected Identifier"), + }, + _ => panic!("Expected Expr"), + } +} + +#[test] +fn test_find_in_augmented_assignment() { + let source = "x += 1"; + let ast = parse(source); + let node = find_node_at_offset(&ast, 0).unwrap(); + match node { + Node::Expr(e) => match &e.kind { + ExprKind::Identifier(s) => assert_eq!(s, "x"), + _ => panic!("Expected Identifier"), + }, + _ => panic!("Expected Expr"), + } + + let node_rhs = find_node_at_offset(&ast, 5).unwrap(); + match node_rhs { + Node::Expr(e) => match &e.kind { + ExprKind::Literal(i) => assert_eq!(*i, eldritch_core::Value::Int(1)), + _ => panic!("Expected Integer"), + }, + _ => panic!("Expected Expr"), + } +} + +#[test] +fn test_find_in_if_else() { + let source = "if True:\n pass\nelse:\n pass"; + let ast = parse(source); + // True is at offset 3 + let node = find_node_at_offset(&ast, 3).unwrap(); + match node { + Node::Expr(e) => match &e.kind { + ExprKind::Identifier(s) => assert_eq!(s, "True"), // True is parsed as Identifier in AST (or Bool depending on version, wait, in Eldritch it might be Identifier or True). Let's check: actually it might be True literal but we can just check if it's Expr. + _ => (), // Accept anything as long as it's the condition + }, + _ => panic!("Expected Expr"), + } + + // Check inside else + // "if True:\n pass\nelse:\n pass" + // 012345678 901234567 890123 4567890 + // else is around 18. pass is around 28. + let node_else = find_node_at_offset(&ast, 28); + // Might be Stmt::Pass + assert!(node_else.is_some()); +} + +#[test] +fn test_find_in_return() { + let source = "def f():\n return 42"; + let ast = parse(source); + let node = find_node_at_offset(&ast, 18).unwrap(); + match node { + Node::Expr(e) => match &e.kind { + ExprKind::Literal(i) => assert_eq!(*i, eldritch_core::Value::Int(42)), + _ => (), + }, + _ => panic!("Expected Expr"), + } +} + +#[test] +fn test_find_in_for() { + let source = "for x in [1, 2]: pass"; + let ast = parse(source); + // [1, 2] is at offset 9 + let node = find_node_at_offset(&ast, 10).unwrap(); + match node { + Node::Expr(e) => match &e.kind { + ExprKind::List(_) => (), + ExprKind::Literal(_) => (), // Might hit the inner element + _ => panic!("Expected List or inner Expr, got {:?}", e.kind), + }, + _ => panic!("Expected Expr"), + } +} + +#[test] +fn test_find_in_def() { + let source = "def f(a: int = 1) -> str:\n pass"; + let ast = parse(source); + + // Offset for 'int' + let node_annot = find_node_at_offset(&ast, 9).unwrap(); + assert!(matches!(node_annot, Node::Expr(_))); + + // Offset for '1' + let node_default = find_node_at_offset(&ast, 15).unwrap(); + assert!(matches!(node_default, Node::Expr(_))); + + // Offset for 'str' + let node_ret = find_node_at_offset(&ast, 21).unwrap(); + assert!(matches!(node_ret, Node::Expr(_))); +} + +#[test] +fn test_find_in_lambda() { + let source = "f = lambda x: x + 1"; + let ast = parse(source); + + // Offset for 'x' in body + let node = find_node_at_offset(&ast, 14).unwrap(); + assert!(matches!(node, Node::Expr(_))); +} + +#[test] +fn test_find_in_comprehension() { + let source = "l = [x for x in y if x > 0]"; + let ast = parse(source); + + // 'y' is around 16 + let node = find_node_at_offset(&ast, 16).unwrap(); + assert!(matches!(node, Node::Expr(_))); + + // 'x > 0' is around 21 + let node_cond = find_node_at_offset(&ast, 21).unwrap(); + assert!(matches!(node_cond, Node::Expr(_))); +} + +#[test] +fn test_find_in_dict_comp() { + let source = "d = {k: v for k in x if k > 0}"; + let ast = parse(source); + + let node_key = find_node_at_offset(&ast, 5).unwrap(); + assert!(matches!(node_key, Node::Expr(_))); + + let node_val = find_node_at_offset(&ast, 8).unwrap(); + assert!(matches!(node_val, Node::Expr(_))); +} + +#[test] +fn test_find_in_fstring() { + let source = "f'hello {x}'"; + let ast = parse(source); + + // 'x' is at offset 9 + let node = find_node_at_offset(&ast, 9).unwrap(); + assert!(matches!(node, Node::Expr(_))); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/conversion.rs b/implants/lib/eldritch/eldritch-core/tests/conversion.rs new file mode 100644 index 000000000..618ac4e32 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/conversion.rs @@ -0,0 +1,154 @@ +extern crate alloc; + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use eldritch_core::Value; +use eldritch_core::conversion::{FromValue, IntoEldritchResult, ToValue}; + +#[test] +fn test_conversion_i64() { + let val = 42i64.to_value(); + assert!(matches!(val, Value::Int(42))); + let extracted: i64 = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, 42); + + // Invalid conversion + let err = i64::from_value(&Value::String(String::from("not int"))).unwrap_err(); + assert!(err.contains("Expected Int, got str")); +} + +#[test] +fn test_conversion_f64() { + let val = 42.5f64.to_value(); + assert!(matches!(val, Value::Float(f) if f == 42.5)); + let extracted: f64 = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, 42.5); + + // Coercion from Int + let extracted_from_int: f64 = FromValue::from_value(&Value::Int(42)).unwrap(); + assert_eq!(extracted_from_int, 42.0); + + // Invalid conversion + let err = f64::from_value(&Value::String(String::from("not float"))).unwrap_err(); + assert!(err.contains("Expected Float or Int, got str")); +} + +#[test] +fn test_conversion_string() { + let val = String::from("hello").to_value(); + assert!(matches!(val, Value::String(ref s) if s == "hello")); + let extracted: String = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, "hello"); + + let err = String::from_value(&Value::Int(1)).unwrap_err(); + assert!(err.contains("Expected String, got int")); +} + +#[test] +fn test_conversion_bool() { + let val = true.to_value(); + assert!(matches!(val, Value::Bool(true))); + let extracted: bool = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, true); + + let err = bool::from_value(&Value::Int(1)).unwrap_err(); + assert!(err.contains("Expected Bool, got int")); +} + +#[test] +fn test_conversion_bytes() { + let bytes = vec![1, 2, 3]; + let val = bytes.clone().to_value(); + assert!(matches!(val, Value::Bytes(ref b) if b == &bytes)); + let extracted: Vec = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, bytes); + + let err = Vec::::from_value(&Value::Int(1)).unwrap_err(); + assert!(err.contains("Expected Bytes, got int")); +} + +#[test] +fn test_conversion_vec() { + let vec = vec![1i64, 2, 3]; + let val = vec.clone().to_value(); + if let Value::List(l) = &val { + let list = l.read(); + assert_eq!(list.len(), 3); + } else { + panic!("Expected list"); + } + + let extracted: Vec = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, vec); + + // From Tuple + let tuple_val = Value::Tuple(vec![Value::Int(1), Value::Int(2), Value::Int(3)]); + let extracted_tuple: Vec = FromValue::from_value(&tuple_val).unwrap(); + assert_eq!(extracted_tuple, vec); + + let err = Vec::::from_value(&Value::Int(1)).unwrap_err(); + assert!(err.contains("Expected List or Tuple, got int")); +} + +#[test] +fn test_conversion_btreemap() { + let mut map = BTreeMap::new(); + map.insert(String::from("a"), 1i64); + map.insert(String::from("b"), 2i64); + + let val = map.clone().to_value(); + if let Value::Dictionary(d) = &val { + let dict = d.read(); + assert_eq!(dict.len(), 2); + } else { + panic!("Expected dict"); + } + + let extracted: BTreeMap = FromValue::from_value(&val).unwrap(); + assert_eq!(extracted, map); + + let err = BTreeMap::::from_value(&Value::Int(1)).unwrap_err(); + assert!(err.contains("Expected Dictionary, got int")); +} + +#[test] +fn test_conversion_option() { + let val_some = Some(42i64).to_value(); + assert!(matches!(val_some, Value::Int(42))); + let extracted_some: Option = FromValue::from_value(&val_some).unwrap(); + assert_eq!(extracted_some, Some(42)); + + let val_none: Option = None; + let val_none_val = val_none.to_value(); + assert!(matches!(val_none_val, Value::None)); + let extracted_none: Option = FromValue::from_value(&val_none_val).unwrap(); + assert_eq!(extracted_none, None); +} + +#[test] +fn test_conversion_unit() { + let val = ().to_value(); + assert!(matches!(val, Value::None)); +} + +#[test] +fn test_conversion_value() { + let val = Value::Int(42); + let to_val = val.clone().to_value(); + assert!(matches!(to_val, Value::Int(42))); + let from_val: Value = FromValue::from_value(&val).unwrap(); + assert!(matches!(from_val, Value::Int(42))); +} + +#[test] +fn test_into_eldritch_result() { + let res: Result = Ok(42); + let val = res.into_eldritch_result().unwrap(); + assert!(matches!(val, Value::Int(42))); + + let err: Result = Err(String::from("error")); + let val_err = err.into_eldritch_result().unwrap_err(); + assert_eq!(val_err, "error"); +} diff --git a/implants/lib/eldritch/eldritch-core/tests/tprint_extended.rs b/implants/lib/eldritch/eldritch-core/tests/tprint_extended.rs new file mode 100644 index 000000000..217315e31 --- /dev/null +++ b/implants/lib/eldritch/eldritch-core/tests/tprint_extended.rs @@ -0,0 +1,68 @@ +extern crate alloc; + +use alloc::sync::Arc; +use eldritch_core::{BufferPrinter, Interpreter}; + +fn check_output_contains(code: &str, expected: &[&str]) { + let printer = Arc::new(BufferPrinter::new()); + let mut interp = Interpreter::new_with_printer(printer.clone()); + + let code_trimmed = code + .lines() + .map(|l| l.trim()) + .collect::>() + .join("\n"); + + if let Err(e) = interp.interpret(&code_trimmed) { + panic!("Interpretation failed for code:\n{}\nError: {}", code, e); + } + + let output = printer.read(); + + for fragment in expected { + if !output.contains(fragment) { + panic!( + "Output did not contain '{}'. Output was:\n{}", + fragment, output + ); + } + } +} + +fn check_error(code: &str, error_fragment: &str) { + let mut interp = Interpreter::new(); + match interp.interpret(code) { + Ok(_) => panic!( + "Expected error containing '{}', but succeeded.", + error_fragment + ), + Err(e) => { + if !e.contains(error_fragment) { + panic!( + "Expected error containing '{}', but got: '{}'", + error_fragment, e + ); + } + } + } +} + +#[test] +fn test_tprint_invalid_args() { + check_error("tprint([1, 2])", "must contain only dictionaries"); +} + +#[test] +fn test_tprint_success() { + check_output_contains("tprint([{\"a\": 1}])", &["| a |", "| 1 |"]); +} + +#[test] +fn test_tprint_empty_args() { + check_error("tprint()", "takes at least 1 argument"); +} + +#[test] +fn test_tprint_invalid_type() { + check_error("tprint(1)", "argument must be a list"); +}