use super::*;
use crate::emacs_core::{Context, format_eval_result};
use crate::test_utils::{load_minimal_gnu_backquote_runtime, runtime_startup_eval_all};
use std::fs;
use std::path::PathBuf;
fn eval_one(src: &str) -> String {
let mut ev = Context::new();
let result = ev.eval_str(src);
format_eval_result(&result)
}
fn eval_all(src: &str) -> Vec<String> {
let mut ev = Context::new();
let forms = crate::emacs_core::value_reader::read_all(src).expect("parse");
forms
.into_iter()
.map(|form| {
let result = ev.eval_form(form);
format_eval_result(&result)
})
.collect()
}
fn eval_all_with(ev: &mut Context, src: &str) -> Vec<String> {
let forms = crate::emacs_core::value_reader::read_all(src).expect("parse");
forms
.into_iter()
.map(|form| {
let result = ev.eval_form(form);
format_eval_result(&result)
})
.collect()
}
fn bootstrap_eval_all(src: &str) -> Vec<String> {
runtime_startup_eval_all(src)
}
fn bootstrap_eval_one(src: &str) -> String {
bootstrap_eval_all(src)
.into_iter()
.last()
.expect("bootstrap eval result")
}
fn eval_first_gnu_form_after_marker(eval: &mut Context, source: &str, marker: &str) {
let start = source
.find(marker)
.unwrap_or_else(|| panic!("missing GNU source marker: {marker}"));
let (form, _) = crate::emacs_core::value_reader::read_one(&source[start..], 0)
.unwrap_or_else(|err| panic!("parse GNU source from {marker} failed: {:?}", err))
.unwrap_or_else(|| panic!("no GNU form found after marker: {marker}"));
eval.eval_form(form)
.unwrap_or_else(|err| panic!("evaluate GNU form {marker} failed: {:?}", err));
}
fn install_bare_elisp_shims(ev: &mut Context) {
let shims = r#"
(defalias 'defun (cons 'macro #'(lambda (name arglist &rest body)
(list 'defalias (list 'quote name) (cons 'function (list (cons 'lambda (cons arglist body))))))))
(defalias 'defmacro (cons 'macro #'(lambda (name arglist &rest body)
(list 'defalias (list 'quote name)
(list 'cons ''macro (cons 'function (list (cons 'lambda (cons arglist body)))))))))
(defalias 'when (cons 'macro #'(lambda (cond &rest body)
(list 'if cond (cons 'progn body)))))
(defalias 'unless (cons 'macro #'(lambda (cond &rest body)
(cons 'if (cons cond (cons nil body))))))
"#;
ev.eval_str(shims).expect("install bare elisp shims");
}
fn load_minimal_autoload_runtime(ev: &mut Context) {
install_bare_elisp_shims(ev);
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest.parent().expect("project root");
let subr_source =
fs::read_to_string(project_root.join("lisp/subr.el")).expect("read GNU subr.el");
eval_first_gnu_form_after_marker(ev, &subr_source, "(defun special-form-p (object)");
let byte_run_source = fs::read_to_string(project_root.join("lisp/emacs-lisp/byte-run.el"))
.expect("read GNU byte-run.el");
eval_first_gnu_form_after_marker(
ev,
&byte_run_source,
"(defmacro eval-when-compile (&rest body)",
);
eval_first_gnu_form_after_marker(
ev,
&byte_run_source,
"(defmacro eval-and-compile (&rest body)",
);
}
fn minimal_autoload_eval_all(src: &str) -> Vec<String> {
let mut ev = Context::new();
load_minimal_autoload_runtime(&mut ev);
eval_all_with(&mut ev, src)
}
fn minimal_autoload_eval_one(src: &str) -> String {
minimal_autoload_eval_all(src)
.into_iter()
.last()
.expect("minimal autoload eval result")
}
fn minimal_backquote_runtime_eval_all(src: &str) -> Vec<String> {
let mut ev = Context::new();
load_minimal_gnu_backquote_runtime(&mut ev);
eval_all_with(&mut ev, src)
}
#[test]
fn autoload_manager_register_and_lookup() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
assert!(!mgr.is_autoloaded("foo"));
mgr.register(AutoloadEntry {
name: "foo".into(),
file: "foo-lib".into(),
docstring: Some("Do foo things.".into()),
interactive: false,
autoload_type: AutoloadType::Function,
});
assert!(mgr.is_autoloaded("foo"));
let entry = mgr.get_entry("foo").unwrap();
assert_eq!(entry.file, "foo-lib");
assert_eq!(entry.docstring.as_deref(), Some("Do foo things."));
assert!(!entry.interactive);
assert_eq!(entry.autoload_type, AutoloadType::Function);
}
#[test]
fn autoload_manager_remove() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.register(AutoloadEntry {
name: "bar".into(),
file: "bar-lib".into(),
docstring: None,
interactive: true,
autoload_type: AutoloadType::Macro,
});
assert!(mgr.is_autoloaded("bar"));
mgr.remove("bar");
assert!(!mgr.is_autoloaded("bar"));
}
#[test]
fn autoload_manager_multiple_entries() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.register(AutoloadEntry {
name: "a".into(),
file: "file-a".into(),
docstring: None,
interactive: false,
autoload_type: AutoloadType::Function,
});
mgr.register(AutoloadEntry {
name: "b".into(),
file: "file-b".into(),
docstring: None,
interactive: false,
autoload_type: AutoloadType::Keymap,
});
assert!(mgr.is_autoloaded("a"));
assert!(mgr.is_autoloaded("b"));
assert!(!mgr.is_autoloaded("c"));
}
#[test]
fn autoload_type_from_value() {
crate::test_utils::init_test_tracing();
assert_eq!(
AutoloadType::from_value(&Value::NIL),
AutoloadType::Function
);
assert_eq!(
AutoloadType::from_value(&Value::symbol("macro")),
AutoloadType::Macro
);
assert_eq!(
AutoloadType::from_value(&Value::symbol("keymap")),
AutoloadType::Keymap
);
assert_eq!(
AutoloadType::from_value(&Value::symbol("unknown")),
AutoloadType::Function
);
}
#[test]
fn autoload_type_roundtrip() {
crate::test_utils::init_test_tracing();
let types = [
AutoloadType::Function,
AutoloadType::Macro,
AutoloadType::Keymap,
];
for ty in &types {
let val = ty.to_value();
let back = AutoloadType::from_value(&val);
assert_eq!(&back, ty);
}
}
#[test]
fn after_load_add_and_take() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.add_after_load("my-file", Value::fixnum(1));
mgr.add_after_load("my-file", Value::fixnum(2));
mgr.add_after_load("other-file", Value::fixnum(3));
let forms = mgr.take_after_load_forms("my-file");
assert_eq!(forms.len(), 2);
let forms2 = mgr.take_after_load_forms("my-file");
assert!(forms2.is_empty());
let forms3 = mgr.take_after_load_forms("other-file");
assert_eq!(forms3.len(), 1);
}
#[test]
fn loaded_files_tracking() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
assert!(!mgr.is_loaded("foo.el"));
mgr.mark_loaded("foo.el");
assert!(mgr.is_loaded("foo.el"));
mgr.mark_loaded("foo.el");
assert!(mgr.is_loaded("foo.el"));
}
#[test]
fn obsolete_function_tracking() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
assert!(!mgr.is_function_obsolete("old-fn"));
mgr.make_obsolete("old-fn", "new-fn", "28.1");
assert!(mgr.is_function_obsolete("old-fn"));
let info = mgr.get_obsolete_function("old-fn").unwrap();
assert_eq!(info.0, "new-fn");
assert_eq!(info.1, "28.1");
}
#[test]
fn obsolete_variable_tracking() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
assert!(!mgr.is_variable_obsolete("old-var"));
mgr.make_variable_obsolete("old-var", "new-var", "27.1");
assert!(mgr.is_variable_obsolete("old-var"));
let info = mgr.get_obsolete_variable("old-var").unwrap();
assert_eq!(info.0, "new-var");
assert_eq!(info.1, "27.1");
}
#[test]
fn is_autoload_value_positive() {
crate::test_utils::init_test_tracing();
let val = Value::list(vec![Value::symbol("autoload"), Value::string("my-file")]);
assert!(is_autoload_value(&val));
}
#[test]
fn is_autoload_value_negative() {
crate::test_utils::init_test_tracing();
assert!(!is_autoload_value(&Value::NIL));
assert!(!is_autoload_value(&Value::fixnum(42)));
assert!(!is_autoload_value(&Value::list(vec![
Value::symbol("lambda"),
Value::NIL,
])));
}
#[test]
fn autoload_special_form_registers() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(autoload 'my-func "my-file" "A function." t)
(let ((f (symbol-function 'my-func)))
(and (consp f) (eq (car f) 'autoload)))"#,
);
assert_eq!(results[0], "OK my-func");
assert_eq!(results[1], "OK t");
}
#[test]
fn autoload_minimal_form() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(autoload 'minimal-fn "min-file")
(let ((f (symbol-function 'minimal-fn)))
(and (consp f) (eq (car f) 'autoload)))"#,
);
assert_eq!(results[0], "OK minimal-fn");
assert_eq!(results[1], "OK t");
}
#[test]
fn autoload_with_type() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(autoload 'my-macro "macro-file" nil nil 'macro)
(let ((f (symbol-function 'my-macro)))
(and (consp f) (eq (car f) 'autoload)))"#,
);
assert_eq!(results[0], "OK my-macro");
assert_eq!(results[1], "OK t");
}
#[test]
fn autoload_is_callable_subr_surface() {
crate::test_utils::init_test_tracing();
let results = minimal_autoload_eval_all(
r#"(fboundp 'autoload)
(special-form-p 'autoload)
(subrp (symbol-function 'autoload))
(subr-arity (symbol-function 'autoload))
(func-arity 'autoload)
(funcall 'autoload 'my-funcall-fn "my-funcall-file")
(let ((f (symbol-function 'my-funcall-fn)))
(and (consp f) (eq (car f) 'autoload)))"#,
);
assert_eq!(results[0], "OK t");
assert_eq!(results[1], "OK nil");
assert_eq!(results[2], "OK t");
assert_eq!(results[3], "OK (2 . 5)");
assert_eq!(results[4], "OK (2 . 5)");
assert_eq!(results[5], "OK my-funcall-fn");
assert_eq!(results[6], "OK t");
}
#[test]
fn autoload_rejects_too_many_arguments() {
crate::test_utils::init_test_tracing();
let result = eval_one(
r#"(condition-case err
(autoload 'too-many "x" nil nil nil nil)
(error (list (car err) (cdr err))))"#,
);
assert_eq!(result, "OK (wrong-number-of-arguments (autoload 6))");
}
#[test]
fn autoload_funcall_type_checks_first_argument() {
crate::test_utils::init_test_tracing();
let result = eval_one(
r#"(condition-case err
(funcall 'autoload 1 "x")
(error (list (car err) (cdr err))))"#,
);
assert_eq!(result, "OK (wrong-type-argument (symbolp 1))");
}
#[test]
fn eval_when_compile_evaluates_body() {
crate::test_utils::init_test_tracing();
let result = minimal_autoload_eval_one("(eval-when-compile (+ 1 2))");
assert_eq!(result, "OK 3");
}
#[test]
fn eval_when_compile_multiple_forms() {
crate::test_utils::init_test_tracing();
let result = minimal_autoload_eval_one("(eval-when-compile 1 2 (+ 3 4))");
assert_eq!(result, "OK 7");
}
#[test]
fn eval_when_compile_propagates_errors() {
crate::test_utils::init_test_tracing();
let result = minimal_autoload_eval_one(
r#"(condition-case err
(eval-when-compile (signal 'error '("boom")))
(error (list (car err) (cdr err))))"#,
);
assert_eq!(result, r#"OK (error ("boom"))"#);
}
#[test]
fn eval_and_compile_evaluates_body() {
crate::test_utils::init_test_tracing();
let result = minimal_autoload_eval_one("(eval-and-compile (+ 10 20))");
assert_eq!(result, "OK 30");
}
#[test]
fn eval_and_compile_multiple_forms() {
crate::test_utils::init_test_tracing();
let result = minimal_autoload_eval_one("(eval-and-compile (setq x 1) (setq y 2) (+ x y))");
assert_eq!(result, "OK 3");
}
#[test]
fn symbol_file_returns_nil() {
crate::test_utils::init_test_tracing();
let result = eval_one("(symbol-file 'cons)");
assert_eq!(result, "OK nil");
}
#[test]
fn symbol_file_returns_autoload_file_for_function() {
crate::test_utils::init_test_tracing();
let result = eval_one(
r#"(progn (autoload 'sym-file-probe "sym-file-probe-file") (symbol-file 'sym-file-probe))"#,
);
assert_eq!(result, r#"OK "sym-file-probe-file""#);
}
#[test]
fn symbol_file_type_gate_matches_defun_only() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(autoload 'sym-file-type-probe "sym-file-type-probe-file")
(symbol-file 'sym-file-type-probe 'defun)
(symbol-file 'sym-file-type-probe 'var)
(symbol-file 'sym-file-type-probe 'function)"#,
);
assert_eq!(results[1], r#"OK "sym-file-type-probe-file""#);
assert_eq!(results[2], "OK nil");
assert_eq!(results[3], "OK nil");
}
#[test]
fn symbol_file_non_symbol_returns_nil() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(symbol-file 1)
(symbol-file "x")
(symbol-file 'car 1)"#,
);
assert_eq!(results[0], "OK nil");
assert_eq!(results[1], "OK nil");
assert_eq!(results[2], "OK nil");
}
#[test]
fn symbol_file_accepts_third_arg_but_not_fourth() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(autoload 'sym-file-arity-probe "sym-file-arity-probe-file")
(symbol-file 'sym-file-arity-probe 'defun t)
(condition-case err
(symbol-file 'sym-file-arity-probe 'defun t :extra)
(error err))"#,
);
assert_eq!(results[1], r#"OK "sym-file-arity-probe-file""#);
assert_eq!(results[2], "OK (wrong-number-of-arguments symbol-file 4)");
}
#[test]
fn autoload_entry_interactive_flag() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.register(AutoloadEntry {
name: "cmd".into(),
file: "cmd-file".into(),
docstring: None,
interactive: true,
autoload_type: AutoloadType::Function,
});
let entry = mgr.get_entry("cmd").unwrap();
assert!(entry.interactive);
}
#[test]
fn autoload_entry_keymap_type() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.register(AutoloadEntry {
name: "my-map".into(),
file: "map-file".into(),
docstring: None,
interactive: false,
autoload_type: AutoloadType::Keymap,
});
let entry = mgr.get_entry("my-map").unwrap();
assert_eq!(entry.autoload_type, AutoloadType::Keymap);
}
#[test]
fn autoload_overwrites_previous() {
crate::test_utils::init_test_tracing();
let mut mgr = AutoloadManager::new();
mgr.register(AutoloadEntry {
name: "f".into(),
file: "old-file".into(),
docstring: None,
interactive: false,
autoload_type: AutoloadType::Function,
});
mgr.register(AutoloadEntry {
name: "f".into(),
file: "new-file".into(),
docstring: None,
interactive: true,
autoload_type: AutoloadType::Macro,
});
let entry = mgr.get_entry("f").unwrap();
assert_eq!(entry.file, "new-file");
assert!(entry.interactive);
assert_eq!(entry.autoload_type, AutoloadType::Macro);
}
#[test]
fn autoload_does_not_override_real_definition() {
crate::test_utils::init_test_tracing();
let results = eval_all(
r#"(defalias 'already-defined #'(lambda () 42))
(autoload 'already-defined "some-file")
;; autoload should return nil (skipped)
;; and the real definition should still be in place
(already-defined)"#,
);
assert_eq!(results[1], "OK nil");
assert_eq!(results[2], "OK 42");
}
#[test]
fn autoload_registers_in_autoload_manager() {
crate::test_utils::init_test_tracing();
let mut ev = Context::new();
let results = eval_all_with(
&mut ev,
r#"(autoload 'test-auto-fn "test-auto-file" "Test doc" t 'macro)"#,
);
assert_eq!(results[0], "OK test-auto-fn");
assert!(ev.autoloads.is_autoloaded("test-auto-fn"));
let entry = ev.autoloads.get_entry("test-auto-fn").unwrap();
assert_eq!(entry.file, "test-auto-file");
assert_eq!(entry.docstring.as_deref(), Some("Test doc"));
assert!(entry.interactive);
assert_eq!(entry.autoload_type, AutoloadType::Macro);
}
#[test]
fn eval_after_load_deferred_fires_on_provide() {
crate::test_utils::init_test_tracing();
let results = minimal_backquote_runtime_eval_all(
r#"(defvar neovm--eal-test-log nil)
(eval-after-load 'neovm--eal-test-feat
'(setq neovm--eal-test-log (cons 'deferred neovm--eal-test-log)))
neovm--eal-test-log
(provide 'neovm--eal-test-feat)
neovm--eal-test-log"#,
);
assert_eq!(results[2], "OK nil");
assert_eq!(results[4], "OK (deferred)");
}
#[test]
fn eval_after_load_immediate_fires_when_already_provided() {
crate::test_utils::init_test_tracing();
let results = minimal_backquote_runtime_eval_all(
r#"(defvar neovm--eal-imm-log nil)
(provide 'neovm--eal-imm-feat)
(eval-after-load 'neovm--eal-imm-feat
'(setq neovm--eal-imm-log (cons 'immediate neovm--eal-imm-log)))
neovm--eal-imm-log"#,
);
assert_eq!(results[3], "OK (immediate)");
}
#[test]
fn with_eval_after_load_fires_when_already_provided() {
crate::test_utils::init_test_tracing();
let results = minimal_backquote_runtime_eval_all(
r#"(defvar neovm--weal-test-result nil)
(provide 'neovm--weal-test-feat)
(with-eval-after-load 'neovm--weal-test-feat
(setq neovm--weal-test-result 'executed))
neovm--weal-test-result"#,
);
assert_eq!(results[3], "OK executed");
}