Files
cadcraft/crates/engine/src/lisp/tests.rs
T
Hamza d9adce038c AutoLISP: = compares the symbols type returns
`(= (type x) 'LIST)` is how an AutoLISP routine tests a type, because `type`
returns a symbol. `=` coerced both of its arguments to numbers, so the symbol
failed the coercion and the routine stopped at its first type test:

  _$ (= (type 1) 'INT)
  bad argument type: numberp: INT

`=` and `/=` now compare numbers and strings as before, and any other atom -- a
symbol, `nil`, an entity name, a selection set, a file handle -- by what it is,
the way `eq` does. Two consequences, both closer to AutoLISP than raising was:
a mismatch of kinds answers `nil` rather than failing, so `(= 1 "1")` and
`(= '(1) '(1))` answer (comparing list structure is `equal`'s job), and
`(= nil nil)` is `T`.

The ordering comparisons still take numbers and strings only, as AutoLISP has
them: `(< 'a 'b)` remains `bad argument type: numberp: A`.

Fix: #1597
2026-10-19 17:26:39 +05:00

655 lines
30 KiB
Rust

#![allow(clippy::unwrap_used)]
use super::value::{Value, to_text};
use crate::Session;
use crate::cmd::file::{EntityCodec, set_entity_codec};
fn session() -> Session {
set_entity_codec(EntityCodec { groups: cadcraft_io::entity_groups, entity: cadcraft_io::entity_from_groups });
Session::new()
}
/// Evaluate `code` to the end; its value as `prin1` shows it, or the error message.
fn eval(s: &mut Session, code: &str) -> Result<String, String> {
eval_with(s, code, Vec::new())
}
fn eval_with(s: &mut Session, code: &str, inputs: Vec<Value>) -> Result<String, String> {
match s.lisp_eval(code, false, true, inputs) {
Ok(Some(Ok(v))) => Ok(to_text(&v, true)),
Ok(Some(Err(e))) => Err(e),
Ok(None) => Err("did not finish".into()),
Err(e) => Err(e.to_string()),
}
}
fn ok(s: &mut Session, code: &str) -> String {
match eval(s, code) {
Ok(v) => v,
Err(e) => panic!("{code}: {e}"),
}
}
#[test]
fn real_print_subnormal_and_rounding() {
let mut s = session();
assert_eq!(ok(&mut s, "1e-310"), "1.0e-310");
assert_eq!(ok(&mut s, "5e-324"), "4.94066e-324");
assert_eq!(ok(&mut s, "9.9999996e10"), "1.0e+011");
assert_eq!(ok(&mut s, "9.9999996e-7"), "1.0e-006");
assert_eq!(super::value::real_text(-1e-310), "-1.0e-310");
assert_eq!(super::value::real_text(1e308), "1.0e+308");
assert_eq!(super::value::real_text(999999.5), "1.0e+006");
assert_eq!(super::value::real_text(0.000009999996), "0.00001");
}
#[test]
fn arithmetic_follows_autolisp() {
let mut s = session();
assert_eq!(ok(&mut s, "(+ 1 2 3)"), "6");
assert_eq!(ok(&mut s, "(+ 1 2.0)"), "3.0");
assert_eq!(ok(&mut s, "(/ 7 2)"), "3");
assert_eq!(ok(&mut s, "(/ 7 2.0)"), "3.5");
assert_eq!(ok(&mut s, "(- 5)"), "-5");
assert_eq!(ok(&mut s, "(rem 7 3)"), "1");
assert_eq!(ok(&mut s, "(1+ 41)"), "42");
assert_eq!(ok(&mut s, "(fix 3.7)"), "3");
assert_eq!(ok(&mut s, "(float 3)"), "3.0");
assert_eq!(ok(&mut s, "(expt 2 10)"), "1024");
assert_eq!(ok(&mut s, "(sqrt 16)"), "4.0");
assert_eq!(ok(&mut s, "(max 1 5 3)"), "5");
assert_eq!(ok(&mut s, "(min 1 5.0 3)"), "1.0");
assert_eq!(ok(&mut s, "(abs -4)"), "4");
assert_eq!(ok(&mut s, "(logand 12 10)"), "8");
assert_eq!(ok(&mut s, "(lsh 1 4)"), "16");
assert_eq!(ok(&mut s, "(+ 2147483647 1)"), "-2147483648", "32-bit integers wrap");
assert_eq!(ok(&mut s, "(< 1 2 3)"), "T");
assert_eq!(ok(&mut s, "(= 1 1.0)"), "T");
assert_eq!(ok(&mut s, "(= \"a\" \"a\")"), "T");
assert_eq!(eval(&mut s, "(/ 1 0)"), Err("divide by zero".into()));
assert_eq!(eval(&mut s, "(+ 1 nil)"), Err("bad argument type: numberp: nil".into()));
}
/// `=` answers for the symbols `type` returns, which is how AutoLISP routines test a type:
/// `(= (type x) 'INT)`. Coercing the symbol to a number failed the whole routine with "bad
/// argument type" instead, so a routine written for AutoLISP stopped at its first type test
/// (#1597). The ordering comparisons are still numbers and strings, as AutoLISP has them.
#[test]
fn equality_compares_symbols_as_autolisp_does() {
let mut s = session();
assert_eq!(ok(&mut s, "(= (type 1) 'INT)"), "T");
assert_eq!(ok(&mut s, "(= (type 1.0) 'INT)"), "nil");
assert_eq!(ok(&mut s, "(= (type '(1 2)) 'LIST)"), "T");
assert_eq!(ok(&mut s, "(= (type \"a\") 'STR)"), "T");
assert_eq!(ok(&mut s, "(/= (type 1) 'REAL)"), "T");
assert_eq!(ok(&mut s, "(= 'a 'a 'a)"), "T");
assert_eq!(ok(&mut s, "(= 'a 'b)"), "nil");
// The routine from the report runs.
ok(&mut s, "(defun isint (a) (if (= (type a) 'INT) T nil))");
assert_eq!(ok(&mut s, "(isint 3)"), "T");
assert_eq!(ok(&mut s, "(isint 3.0)"), "nil");
assert_eq!(ok(&mut s, "(isint \"3\")"), "nil");
// A mismatch of kinds is nil, not an error; numbers and strings are unchanged.
assert_eq!(ok(&mut s, "(= 1 \"1\")"), "nil");
assert_eq!(ok(&mut s, "(= '(1) '(1))"), "nil", "`=` is not `equal`");
assert_eq!(ok(&mut s, "(= nil nil)"), "T");
assert_eq!(ok(&mut s, "(= 1 1.0)"), "T");
assert_eq!(ok(&mut s, "(= \"a\" \"a\" \"a\")"), "T");
assert_eq!(ok(&mut s, "(= 1 1 2)"), "nil");
// Ordering still refuses what AutoLISP refuses.
assert_eq!(eval(&mut s, "(< 'a 'b)"), Err("bad argument type: numberp: A".into()));
}
#[test]
fn lists_and_strings() {
let mut s = session();
assert_eq!(ok(&mut s, "(cons 1 '(2 3))"), "(1 2 3)");
assert_eq!(ok(&mut s, "(cons 8 \"0\")"), "(8 . \"0\")");
assert_eq!(ok(&mut s, "(cadr '(1 2 3))"), "2");
assert_eq!(ok(&mut s, "(cdr (assoc 40 '((10 1.0 2.0 0.0) (40 . 2.5))))"), "2.5");
assert_eq!(ok(&mut s, "(subst '(40 . 5.0) '(40 . 2.5) '((8 . \"0\") (40 . 2.5)))"), "((8 . \"0\") (40 . 5.0))");
assert_eq!(ok(&mut s, "(append '(1) '(2 3) nil '(4))"), "(1 2 3 4)");
assert_eq!(ok(&mut s, "(reverse '(1 2 3))"), "(3 2 1)");
assert_eq!(ok(&mut s, "(nth 1 '(a b c))"), "B");
assert_eq!(ok(&mut s, "(member 2 '(1 2 3))"), "(2 3)");
assert_eq!(ok(&mut s, "(length '(1 2 3))"), "3");
assert_eq!(ok(&mut s, "(last '(1 2 3))"), "3");
assert_eq!(ok(&mut s, "(mapcar '+ '(1 2 3) '(10 20 30))"), "(11 22 33)");
assert_eq!(ok(&mut s, "(mapcar '(lambda (x) (* x x)) '(1 2 3))"), "(1 4 9)");
assert_eq!(ok(&mut s, "(apply 'max '(3 9 2))"), "9");
assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) '<)"), "(1 2 3)");
assert_eq!(ok(&mut s, "(vl-sort '(3 1 2) (function (lambda (a b) (> a b))))"), "(3 2 1)");
assert_eq!(ok(&mut s, "(vl-remove-if 'minusp '(1 -2 3))"), "(1 3)");
assert_eq!(ok(&mut s, "(strcat \"a\" \"b\" \"c\")"), "\"abc\"");
assert_eq!(ok(&mut s, "(substr \"Hello\" 2 3)"), "\"ell\"");
assert_eq!(ok(&mut s, "(strcase \"abc\")"), "\"ABC\"");
assert_eq!(ok(&mut s, "(strlen \"abc\" \"de\")"), "5");
assert_eq!(ok(&mut s, "(itoa 42)"), "\"42\"");
assert_eq!(ok(&mut s, "(atoi \"12abc\")"), "12");
assert_eq!(ok(&mut s, "(atof \"2.5mm\")"), "2.5");
assert_eq!(ok(&mut s, "(atof \"x\")"), "0.0");
assert_eq!(ok(&mut s, "(wcmatch \"Name\" \"N*\")"), "T");
assert_eq!(ok(&mut s, "(wcmatch \"A12\" \"@##\")"), "T");
assert_eq!(ok(&mut s, "(wcmatch \"abc\" \"x*,a?c\")"), "T");
assert_eq!(ok(&mut s, "(wcmatch \"abc\" \"~a*\")"), "nil");
assert_eq!(ok(&mut s, "(wcmatch \"b\" \"[a-c]\")"), "T");
assert_eq!(ok(&mut s, "(vl-string-subst \"X\" \"b\" \"abcb\")"), "\"aXcb\"");
assert_eq!(ok(&mut s, "(vl-string-search \"c\" \"abc\")"), "2");
assert_eq!(ok(&mut s, "(read \"(1 2)\")"), "(1 2)");
assert_eq!(ok(&mut s, "(type \"x\")"), "STR");
assert_eq!(ok(&mut s, "(type 'x)"), "SYM");
assert_eq!(ok(&mut s, "(equal '(1 2.0) '(1 2.0000001) 1e-3)"), "T");
}
#[test]
fn geometry() {
let mut s = session();
assert_eq!(ok(&mut s, "(polar '(0 0) 0 5)"), "(5.0 0.0)");
assert_eq!(ok(&mut s, "(distance '(0 0) '(3 4))"), "5.0");
assert_eq!(ok(&mut s, "(angle '(0 0) '(0 1))"), "1.5708");
assert_eq!(ok(&mut s, "(inters '(0 0) '(10 10) '(0 10) '(10 0))"), "(5.0 5.0)");
assert_eq!(ok(&mut s, "(inters '(0 0) '(1 1) '(0 10) '(10 0))"), "nil");
assert_eq!(ok(&mut s, "(inters '(0 0) '(1 1) '(0 10) '(10 0) nil)"), "(5.0 5.0)");
}
#[test]
fn functions_scope_and_control() {
let mut s = session();
ok(&mut s, "(defun sq (x) (* x x))");
assert_eq!(ok(&mut s, "(sq 7)"), "49");
ok(&mut s, "(defun fact (n) (if (< n 2) 1 (* n (fact (1- n)))))");
assert_eq!(ok(&mut s, "(fact 10)"), "3628800");
// Dynamic scope: arguments and locals hide globals while the function runs.
ok(&mut s, "(setq x 1 y 2)");
ok(&mut s, "(defun f (x / y) (setq y 5) (g))");
ok(&mut s, "(defun g () (list x y))");
assert_eq!(ok(&mut s, "(f 9)"), "(9 5)");
assert_eq!(ok(&mut s, "(list x y)"), "(1 2)", "restored after the call");
assert_eq!(ok(&mut s, "(progn (setq i 0 acc nil) (while (< i 3) (setq acc (cons i acc) i (1+ i))) acc)"), "(2 1 0)");
assert_eq!(ok(&mut s, "(progn (setq n 0) (repeat 4 (setq n (+ n 2))) n)"), "8");
assert_eq!(ok(&mut s, "(progn (setq tot 0) (foreach v '(1 2 3) (setq tot (+ tot v))) tot)"), "6");
assert_eq!(ok(&mut s, "(cond ((= 1 2) 'a) ((= 1 1) 'b) (t 'c))"), "B");
assert_eq!(ok(&mut s, "(and 1 2)"), "T");
assert_eq!(ok(&mut s, "(or nil nil)"), "nil");
assert_eq!(ok(&mut s, "((lambda (a b) (+ a b)) 1 2)"), "3");
assert_eq!(ok(&mut s, "(eval '(+ 1 2))"), "3");
assert_eq!(eval(&mut s, "(sq)"), Err("too few arguments".into()));
assert_eq!(eval(&mut s, "(sq 1 2)"), Err("too many arguments".into()));
assert_eq!(eval(&mut s, "(nosuchfunction 1)"), Err("no function definition: NOSUCHFUNCTION".into()));
}
#[test]
fn errors_unwind_bindings_and_reach_the_handler() {
let mut s = session();
ok(&mut s, "(setq v 'outer)");
ok(&mut s, "(defun boom (v) (/ 1 0))");
assert!(eval(&mut s, "(boom 'inner)").is_err());
assert_eq!(ok(&mut s, "v"), "OUTER", "the error restored the binding");
// *error* runs with the message.
ok(&mut s, "(defun *error* (msg) (setq caught msg))");
assert!(eval(&mut s, "(boom 1)").is_err());
assert_eq!(ok(&mut s, "caught"), "\"divide by zero\"");
ok(&mut s, "(setq *error* nil)");
// vl-catch-all-apply turns an error into a value.
assert_eq!(ok(&mut s, "(vl-catch-all-error-message (vl-catch-all-apply '/ '(1 0)))"), "\"divide by zero\"");
assert_eq!(ok(&mut s, "(vl-catch-all-apply '+ '(1 2))"), "3");
assert!(eval(&mut s, "(exit)").is_err());
}
#[test]
fn runaway_routines_are_errors_not_crashes() {
let mut s = session();
ok(&mut s, "(defun deep (n) (+ 1 (deep (1+ n))))");
assert_eq!(eval(&mut s, "(deep 0)"), Err("internal stack limit reached".into()));
// A deeply nested source text.
let nested = format!("{}1{}", "(+ ".repeat(50_000), ")".repeat(50_000));
assert!(eval(&mut s, &nested).is_err());
// Hostile values.
assert!(eval(&mut s, "(substr \"abc\" 0)").is_err());
assert!(eval(&mut s, "(nth -1 '(1))").is_ok());
assert!(eval(&mut s, "(chr -5)").is_ok());
assert!(eval(&mut s, "(expt 2.0 99999)").is_err());
assert!(eval(&mut s, "(gcd 0 0)").is_err());
assert!(eval(&mut s, "(wcmatch \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab\" \"*a*a*a*a*a*a*a*a*a*c\")").is_ok());
assert!(eval(&mut s, "(fix 1e300)").is_ok());
assert!(eval(&mut s, "(lsh 1 99)").is_ok());
// An endless loop in a blocking run stops at the step cap.
let r = eval(&mut s, "(setq n 0) (while (< n 3000000) (setq n (1+ n))) n");
assert_eq!(r, Ok("3000000".into()));
}
#[test]
fn printing() {
let mut s = session();
let before = s.log.len();
ok(&mut s, "(princ \"Hello\") (princ \" world\\n\") (prin1 \"q\") (terpri) (print 3) (princ)");
let out: Vec<_> = s.log[before..].to_vec();
assert_eq!(out, ["Hello world", "\"q\"", "", "3 "]);
assert_eq!(ok(&mut s, "(vl-prin1-to-string '(1 \"a\"))"), "\"(1 \\\"a\\\")\"");
assert_eq!(ok(&mut s, "(rtos 12.5 2 2)"), "\"12.50\"");
}
#[test]
fn command_draws_and_one_routine_is_one_undo_step() {
let mut s = session();
let n = |s: &Session| s.doc().unwrap().model.handles().len();
let undo_before = s.state().unwrap().undo.len();
ok(&mut s, "(command \"_.LINE\" '(0 0) '(10 0) \"\") (command \"_.CIRCLE\" '(5 5) 2)");
assert_eq!(n(&s), 2);
assert_eq!(s.state().unwrap().undo.len(), undo_before + 1, "the whole routine undoes at once");
// A command left open carries on in the next call.
ok(&mut s, "(command \"_.LINE\" '(0 10) '(10 10)) (command '(10 20) \"\")");
assert_eq!(n(&s), 4);
assert!(s.running.is_none());
// (defun c:…) makes a command.
ok(&mut s, "(defun c:twolines () (command \"_.LINE\" '(0 0) '(0 5) '(5 5) \"\") (princ))");
s.cmdline("TWOLINES").unwrap();
assert_eq!(n(&s), 6);
// An expression typed at Command: shows its value.
s.cmdline("(+ 40 2)").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("42"));
s.cmdline("(setq dist 7.5)").unwrap();
s.cmdline("!dist").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("7.5"));
// An expression at a command's prompt answers it.
s.cmdline("CIRCLE 0,0 (* 2 dist)").unwrap();
let r = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::Circle(c) if c.center.x == 0.0 && c.center.y == 0.0 => Some(c.radius),
_ => None,
});
assert_eq!(r, Some(15.0));
}
#[test]
fn entity_lists_round_trip() {
let mut s = session();
let made = ok(&mut s, "(entmake '((0 . \"CIRCLE\") (8 . \"WALLS\") (10 1.0 2.0 0.0) (40 . 3.0)))");
assert!(made.contains("CIRCLE"));
assert!(s.doc().unwrap().layer("WALLS").is_some(), "entmake creates the layer");
assert_eq!(ok(&mut s, "(cdr (assoc 0 (entget (entlast))))"), "\"CIRCLE\"");
assert_eq!(ok(&mut s, "(cdr (assoc 8 (entget (entlast))))"), "\"WALLS\"");
assert_eq!(ok(&mut s, "(cdr (assoc 10 (entget (entlast))))"), "(1.0 2.0 0.0)");
assert_eq!(ok(&mut s, "(cdr (assoc 410 (entget (entlast))))"), "\"Model\"");
// entmod changes it in place.
ok(&mut s, "(setq d (entget (entlast))) (entmod (subst (cons 40 9.0) (assoc 40 d) d))");
assert_eq!(ok(&mut s, "(cdr (assoc 40 (entget (entlast))))"), "9.0");
// A list without a layer uses the current one.
ok(&mut s, "(setvar \"CLAYER\" \"WALLS\") (entmake '((0 . \"LINE\") (10 0 0 0) (11 5 0 0)))");
assert_eq!(ok(&mut s, "(cdr (assoc 8 (entget (entlast))))"), "\"WALLS\"");
// entnext walks in creation order; entdel deletes and brings back.
assert_eq!(ok(&mut s, "(cdr (assoc 0 (entget (entnext))))"), "\"CIRCLE\"");
assert_eq!(ok(&mut s, "(entnext (entlast))"), "nil");
ok(&mut s, "(setq e (entlast)) (entdel e)");
assert_eq!(ok(&mut s, "(entget e)"), "nil");
ok(&mut s, "(entdel e)");
assert_eq!(ok(&mut s, "(cdr (assoc 0 (entget e)))"), "\"LINE\"");
assert_eq!(ok(&mut s, "(entmake '((0 . \"NOSUCHTHING\")))"), "nil");
assert_eq!(ok(&mut s, "(handent \"ZZZZZZ\")"), "nil");
}
#[test]
fn entity_lists_hold_angles_in_radians() {
let mut s = session();
let near = |s: &mut Session, expr: &str, want: &str| {
let code = format!("(equal {expr} {want} 1e-9)");
assert_eq!(ok(s, &code), "T", "{expr} is {}, not {want}", ok(s, expr));
};
// A DXF file holds an ARC's 50 and 51 in degrees; entget gives radians (#1165).
s.script("ARC\nC\n0,0\n10,0\n0,10\n").unwrap();
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "0.0");
near(&mut s, "(cdr (assoc 51 (entget (entlast))))", "(/ pi 2)");
// ssget filters compare against the values entget shows.
assert_eq!(ok(&mut s, "(sslength (ssget \"X\" '((0 . \"ARC\") (-4 . \"<\") (51 . 2.0))))"), "1");
// entmod takes radians.
ok(&mut s, "(setq d (entget (entlast))) (entmod (subst (cons 51 pi) (assoc 51 d) d))");
let end = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::Arc(a) => Some(a.end),
_ => None,
});
assert!(end.is_some_and(|a| (a - std::f64::consts::PI).abs() < 1e-9), "{end:?}");
// TEXT rotation (50) and oblique angle (51), through entmake.
ok(&mut s, "(entmake (list '(0 . \"TEXT\") '(10 0.0 0.0 0.0) '(40 . 2.5) '(1 . \"abc\") (cons 50 (/ pi 6)) '(51 . 0.25)))");
let text = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::Text(t) => Some((t.rotation, t.oblique)),
_ => None,
});
assert!(text.is_some_and(|(r, o)| (r - std::f64::consts::FRAC_PI_6).abs() < 1e-9 && (o - 0.25).abs() < 1e-9), "{text:?}");
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "(/ pi 6)");
near(&mut s, "(cdr (assoc 51 (entget (entlast))))", "0.25");
// HATCH pattern angle (52) and pattern line angle (53): ANSI31 at 0 has its lines at 45°.
s.script("CIRCLE\n0,0\n10\n-HATCH\nP\nANSI31\n1\n0\n0,0\n\n").unwrap();
assert_eq!(ok(&mut s, "(cdr (assoc 0 (entget (entlast))))"), "\"HATCH\"");
near(&mut s, "(cdr (assoc 52 (entget (entlast))))", "0.0");
near(&mut s, "(cdr (assoc 53 (entget (entlast))))", "(/ pi 4)");
// MTEXT: 50 before the column type (75) is the rotation, after it a column height.
ok(
&mut s,
"(entmake (list '(0 . \"MTEXT\") '(10 0.0 0.0 0.0) '(40 . 2.5) '(41 . 10.0) '(1 . \"abc\") (cons 50 (/ pi 3)) '(75 . 2) '(76 . 2) '(79 . 0) '(48 . 10.0) '(49 . 1.0) '(50 . 30.0) '(50 . 20.0)))",
);
let mtext = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::MText(t) => Some((t.rotation, t.columns.as_ref().map(|c| c.heights.clone()))),
_ => None,
});
assert!(
mtext.as_ref().is_some_and(|(r, h)| (r - std::f64::consts::FRAC_PI_3).abs() < 1e-9 && h.as_deref() == Some(&[30.0, 20.0][..])),
"{mtext:?}"
);
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "(/ pi 3)");
let d = ok(&mut s, "(entget (entlast))");
assert!(d.contains("(50 . 30.0) (50 . 20.0)"), "{d}");
}
#[test]
fn selection_sets_and_filters() {
let mut s = session();
ok(&mut s, "(command \"_.CIRCLE\" '(0 0) 1) (command \"_.CIRCLE\" '(10 0) 2) (command \"_.LINE\" '(0 5) '(20 5) \"\")");
assert_eq!(ok(&mut s, "(sslength (ssget \"_X\" '((0 . \"CIRCLE\"))))"), "2");
assert_eq!(ok(&mut s, "(sslength (ssget \"X\" '((0 . \"CIRCLE,LINE\"))))"), "3");
assert_eq!(ok(&mut s, "(sslength (ssget \"X\" '((-4 . \"<OR\") (0 . \"LINE\") (40 . 2.0) (-4 . \"OR>\"))))"), "2");
assert_eq!(ok(&mut s, "(sslength (ssget \"X\" '((0 . \"CIRCLE\") (-4 . \">\") (40 . 1.5))))"), "1");
assert_eq!(ok(&mut s, "(ssget \"X\" '((0 . \"ARC\")))"), "nil");
assert_eq!(ok(&mut s, "(sslength (ssget \"_W\" '(-5 -5) '(5 5)))"), "1");
assert_eq!(ok(&mut s, "(sslength (ssget \"_C\" '(-5 -5) '(5 6)))"), "2");
assert_eq!(ok(&mut s, "(setq ss (ssadd)) (ssadd (entlast) ss) (sslength ss)"), "1");
assert_eq!(ok(&mut s, "(ssdel (entlast) ss) (sslength ss)"), "0");
assert_eq!(ok(&mut s, "(ssname (ssget \"X\") 99)"), "nil");
}
#[test]
fn tables_and_variables() {
let mut s = session();
ok(&mut s, "(command \"_.-LAYER\" \"_M\" \"DOORS\" \"_C\" \"1\" \"\" \"\")");
assert_eq!(ok(&mut s, "(cdr (assoc 2 (tblsearch \"LAYER\" \"doors\")))"), "\"DOORS\"");
assert_eq!(ok(&mut s, "(tblsearch \"LAYER\" \"nope\")"), "nil");
assert_eq!(ok(&mut s, "(cdr (assoc 0 (tblnext \"LAYER\" t)))"), "\"LAYER\"");
assert_eq!(ok(&mut s, "(getvar \"CLAYER\")"), "\"DOORS\"");
assert_eq!(ok(&mut s, "(setvar \"OSMODE\" 0)"), "0");
assert_eq!(ok(&mut s, "(getvar \"OSMODE\")"), "0");
assert_eq!(ok(&mut s, "(getvar \"NOSUCHVAR\")"), "nil");
}
#[test]
fn input_functions_take_supplied_answers() {
let mut s = session();
let r = eval_with(
&mut s,
"(list (getpoint \"\\nPick: \") (getreal) (getstring) (getint))",
vec![Value::point(&[1.0, 2.0]), Value::Real(2.5), Value::str("hi"), Value::Int(3)],
);
assert_eq!(r, Ok("((1.0 2.0) 2.5 \"hi\" 3)".into()));
// No answer in a programmatic run: nil.
assert_eq!(eval(&mut s, "(getpoint)"), Ok("nil".into()));
}
#[test]
fn interactive_routine_waits_for_the_user() {
let mut s = session();
s.cmdline("(defun c:pt () (setq p (getpoint \"\\nWhere: \")) (command \"_.CIRCLE\" p 1) (princ))").unwrap();
s.cmdline("PT").unwrap();
assert!(s.lisp_running());
assert_eq!(s.prompt_text(), "Where: ");
s.input(crate::Input::Point(cadcraft_geom::Vec2::new(3.0, 4.0))).unwrap();
assert!(!s.lisp_running());
assert!(s.doc().unwrap().model.iter().any(|e| matches!(&e.kind, cadcraft_doc::EntityKind::Circle(c) if c.center.x == 3.0 && c.center.y == 4.0)));
// Typed answers and keywords.
s.cmdline("(progn (initget 1 \"Yes No\") (setq ans (getkword \"\\nSure? [Yes/No]: \")))").unwrap();
s.cmdline("n").unwrap();
assert_eq!(to_text(&s.lisp_var("ANS"), true), "\"No\"");
s.cmdline("(setq d (getdist '(0 0)))").unwrap();
s.cmdline("3,4").unwrap();
assert_eq!(to_text(&s.lisp_var("D"), true), "5.0");
// Esc cancels the routine.
s.cmdline("(setq z (getint))").unwrap();
s.cancel();
assert!(!s.lisp_running());
// `pause` lets the user answer one prompt of a command.
s.cmdline("(command \"_.CIRCLE\" pause 2)").unwrap();
assert!(s.lisp_running());
s.input(crate::Input::Point(cadcraft_geom::Vec2::new(50.0, 50.0))).unwrap();
assert!(!s.lisp_running());
assert!(s.doc().unwrap().model.iter().any(|e| matches!(&e.kind, cadcraft_doc::EntityKind::Circle(c) if c.center.x == 50.0 && c.radius == 2.0)));
}
#[test]
fn appload_and_load() {
let mut s = session();
let r =
s.execute("appload", &serde_json::json!({"name": "tools.lsp", "text": "(defun c:hi () (princ \"hi\") (princ)) (setq loaded 1)"})).unwrap();
assert!(r.get("error").is_none(), "{r}");
assert!(s.log.iter().any(|l| l == "tools.lsp successfully loaded."));
s.cmdline("HI").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("hi"));
// (load) finds a file APPLOAD loaded, by name.
assert_eq!(ok(&mut s, "(setq loaded 0) (load \"tools\") loaded"), "1");
assert!(eval(&mut s, "(load \"no-such-file-anywhere\")").is_err());
assert_eq!(ok(&mut s, "(load \"no-such-file-anywhere\" 'missing)"), "MISSING");
let r = s.execute("lisp", &serde_json::json!({"code": "(princ \"x\") (* 6 7)"})).unwrap();
assert_eq!(r["value"], "42");
assert_eq!(r["output"][0], "x");
let r = s.execute("lisp", &serde_json::json!({"code": "(car 1)"})).unwrap();
assert!(r["error"].as_str().is_some_and(|e| e.contains("bad argument type")));
}
#[test]
fn hostile_lisp_never_panics() {
let mut s = session();
for code in [
"(",
")",
"'",
"\"",
"(1 . 2 3)",
"(quote)",
"(setq)",
"(setq 1 2)",
"(if)",
"(defun)",
"(defun f)",
"(lambda)",
"(repeat 1.5)",
"(repeat -3 1)",
"(foreach 1 2)",
"(mapcar)",
"(mapcar 'car 1)",
"(apply 'car 1)",
"(entget 1)",
"(entmake '(1 2))",
"(entmod nil)",
"(ssget \"Q\")",
"(ssget \"X\" '((-4 . \"<OR\")))",
"(ssname 1 2)",
"(command 1.5 nil '(a b))",
"(tblsearch 1 2)",
"(rtos 1 9)",
"(getvar 1)",
"(setvar \"OSMODE\" \"x\")",
"(open \"\" \"z\")",
"(read-line 5)",
"(close 5)",
"(vl-sort 1 2)",
"(eval (read \"(\"))",
"(substr \"\" 1 -1)",
"(nth 99999999 '(1))",
"(polar 1 2 3)",
"(inters '(0 0) '(0 0) '(0 0) '(0 0))",
"(chr 1114112)",
] {
let _ = eval(&mut s, code);
}
assert!(!s.lisp_running());
}
#[test]
fn a_waiting_routine_is_a_prompt_with_a_rubber_band() {
let mut s = session();
assert!(!s.prompting());
s.cmdline("(setq c (getcorner '(0 0) \"\nOther corner: \"))").unwrap();
// No command runs, yet a prompt waits: clicks must answer it (not select).
assert!(s.running.is_none() && s.prompting());
let current = s.current_prompt().unwrap();
assert!(current.raw && current.display() == "Other corner: ");
// getcorner drags a rectangle from its corner.
let band = s.preview(cadcraft_geom::Vec2::new(3.0, 2.0));
assert_eq!(band.len(), 4);
s.input(crate::Input::Point(cadcraft_geom::Vec2::new(3.0, 2.0))).unwrap();
assert!(!s.prompting());
// getpoint with a base point drags a line; without one, nothing.
s.cmdline("(getpoint '(1 1))").unwrap();
assert_eq!(s.preview(cadcraft_geom::Vec2::new(4.0, 5.0)).len(), 1);
s.cancel();
s.cmdline("(getpoint)").unwrap();
assert!(s.preview(cadcraft_geom::Vec2::new(4.0, 5.0)).is_empty());
s.cancel();
}
#[test]
fn a_leading_newline_ends_the_command_line_instead_of_adding_an_empty_one() {
let mut s = session();
let from = s.log.len();
s.cmdline("(progn (princ \"\nHello\") (princ \"\n\nAfter a blank\") (princ))").unwrap();
assert_eq!(&s.log[from..], ["Hello", "", "After a blank"]);
}
#[test]
fn startup_files_load_from_the_support_folder() {
let dir = std::env::temp_dir().join(format!("cadcraft-lisp-startup-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("acad.lsp"), "(defun c:fromacad () (princ \"acad\") (princ))").unwrap();
std::fs::write(
dir.join("acaddoc.lsp"),
"(setq docvar 1) (autoload \"lazy\" '(\"LAZY\")) (defun S::STARTUP () (setq started (1+ (if started started 0))))",
)
.unwrap();
std::fs::write(dir.join("lazy.lsp"), "(defun c:lazy () (setq lazy-ran 1) (princ \"lazy ran\") (princ))").unwrap();
// A drawing's own folder is never read: an acaddoc.lsp next to it must not run.
let drawings = dir.join("drawings");
std::fs::create_dir_all(&drawings).unwrap();
std::fs::write(drawings.join("acaddoc.lsp"), "(setq evil 1)").unwrap();
let mut s = session();
s.set_lisp_support_dir(Some(dir.clone()));
s.lisp_tick();
let v = |s: &Session, n: &str| to_text(&s.lisp_var(n), true);
assert_eq!(v(&s, "DOCVAR"), "1");
assert_eq!(v(&s, "STARTED"), "1", "S::STARTUP ran once");
assert!(matches!(s.lisp_var("C:FROMACAD"), Value::Func(_)));
s.lisp_tick();
assert_eq!(v(&s, "STARTED"), "1", "a drawing gets its startup files once");
// autoload: the command loads its file on first use.
s.cmdline("LAZY").unwrap();
assert_eq!(v(&s, "LAZY-RAN"), "1");
assert_eq!(s.log.last().map(String::as_str), Some("lazy ran"));
// A new drawing: acaddoc.lsp again, acad.lsp only once per session...
s.new_drawing(false);
s.lisp_tick();
assert_eq!(v(&s, "DOCVAR"), "1");
assert!(s.lisp_var("C:FROMACAD").is_nil());
// ...unless ACADLSPASDOC is on.
crate::sysvars::set(&mut s, "ACADLSPASDOC", &serde_json::json!(1)).unwrap();
s.new_drawing(false);
s.lisp_tick();
assert!(matches!(s.lisp_var("C:FROMACAD"), Value::Func(_)));
// A drawing opened from a folder with its own acaddoc.lsp: only the support folder's runs.
let path = drawings.join("plan.dxf").to_string_lossy().into_owned();
s.open_drawing(cadcraft_doc::Drawing::new_metric(), "plan.dxf", Some(path));
s.lisp_tick();
assert_eq!(v(&s, "DOCVAR"), "1");
assert!(s.lisp_var("EVIL").is_nil());
// No support folder: nothing loads.
let mut s = session();
s.lisp_tick();
assert!(s.lisp_var("DOCVAR").is_nil());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn deeply_nested_lists_drop_without_overflowing_the_stack() {
let _heavy = super::value::HEAVY_CELLS.lock().unwrap_or_else(|e| e.into_inner());
// Nested in the car a million deep: dropping it must not recurse.
let mut v = Value::Nil;
for _ in 0..1_000_000 {
v = Value::list(vec![v]);
}
drop(v);
// Functions nested in function bodies too.
let mut f = Value::Nil;
for _ in 0..200_000 {
f = Value::Func(std::sync::Arc::new(super::value::Func { name: None, params: Vec::new(), locals: Vec::new(), body: Value::list(vec![f]) }));
}
drop(f);
// The same built by a routine and dropped when the variable is cleared.
let mut s = session();
assert_eq!(ok(&mut s, "(setq x nil) (repeat 100000 (setq x (list x))) (setq x nil) 1"), "1");
}
#[test]
fn too_many_list_cells_stop_the_routine() {
let _heavy = super::value::HEAVY_CELLS.lock().unwrap_or_else(|e| e.into_inner());
let mut s = session();
s.lisp.limits.cells = 50_000;
let e = eval(&mut s, "(setq l nil) (repeat 2000000 (setq l (cons 1 l)))").unwrap_err();
assert!(e.contains("too many list cells"), "{e}");
// The variable still holds what was made, but the drawing's LISP stays usable.
assert_eq!(ok(&mut s, "(setq l nil) (+ 1 2)"), "3");
// A doubling list is stopped as well.
let e = eval(&mut s, "(setq l '(1)) (repeat 100 (setq l (append l l)))").unwrap_err();
assert!(e.contains("too many list cells"), "{e}");
assert_eq!(ok(&mut s, "(setq l nil) (length '(1 2 3))"), "3");
}
#[test]
fn strings_are_capped() {
let _heavy = super::value::HEAVY_CELLS.lock().unwrap_or_else(|e| e.into_inner());
let mut s = session();
// A string doubled until it would not fit in memory.
let e = eval(&mut s, "(setq s \"x\") (repeat 100 (setq s (strcat s s)))").unwrap_err();
assert!(e.contains("string longer than"), "{e}");
// Printing a list that shares its parts (exponentially long text) is cut.
let t = ok(&mut s, "(setq x '(1 2)) (repeat 40 (setq x (list x x))) (strlen (vl-prin1-to-string x))");
assert!(t.parse::<usize>().unwrap() <= super::value::MAX_STRING + 64, "{t}");
ok(&mut s, "(setq x nil s nil)");
// Many strings, each short, add up to the run's budget.
s.lisp.limits.string_bytes = 1_000_000;
let e = eval(&mut s, "(setq a \"0123456789\") (repeat 20000 (setq b (strcat a a a a a a a a a a)))").unwrap_err();
assert!(e.contains("too much text"), "{e}");
let e = eval(&mut s, "(repeat 200000 (princ \"0123456789\"))").unwrap_err();
assert!(e.contains("too much text"), "{e}");
// The budget is per run: the next one starts afresh.
assert_eq!(ok(&mut s, "(strcat \"a\" \"b\")"), "\"ab\"");
}
#[test]
fn a_panic_inside_a_routine_ends_it_with_an_error() {
let mut s = session();
let n = |s: &Session| s.doc().unwrap().model.handles().len();
let before = n(&s);
let e = eval(&mut s, "(entmake '((0 . \"LINE\") (10 0 0 0) (11 5 0 0))) (cadcraft-test-panic)").unwrap_err();
assert!(e.contains("internal error"), "{e}");
assert_eq!(n(&s), before, "the drawing is put back");
assert_eq!(ok(&mut s, "(+ 1 2)"), "3");
// A routine carried on by the per-frame tick.
let r = s.lisp_eval("(repeat 500000 (setq i 1)) (cadcraft-test-panic)", false, false, Vec::new()).unwrap();
assert!(r.is_none(), "the routine gave the interface a frame");
assert!(s.lisp_busy());
for _ in 0..100 {
if !s.lisp_busy() {
break;
}
s.lisp_tick();
}
assert!(!s.lisp_running());
assert!(s.log.iter().any(|l| l.contains("internal error")), "{:?}", s.log);
assert_eq!(ok(&mut s, "(+ 1 2)"), "3");
}