krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/ElispTests.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Tests/OrgCoreTests/ElispTests.swift history · blame · raw

96 lines · 5967 bytes

 1import Foundation
 2import Testing
 3@testable import OrgCore
 4
 5/// The Lisp evaluator against Emacs: each form's `prin1` text, or `error` when it signals.
 6struct ElispTests {
 7    static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
 8
 9    static let forms = [
10        "(+ 1 2 3)", "(- 10)", "(- 10 2.5)", "(* 2 3.0)", "(/ 7 2)", "(/ -7 2)", "(/ 7 2.0)", "(/ 7 0)", "(% -7 2)", "(mod -7 2)",
11        "(mod 5.5 2)", "(1+ 4)", "(abs -3.5)", "(max 1 2.0)", "(min 3 1)", "(floor 2.7)", "(floor -7 2)", "(round 2.5)",
12        "(round 3.5)", "(truncate -2.7)", "(float 3)", "(= 1 1.0)", "(< 1 2 3)", "(< 1 3 2)", "(/= 1 2)", "(zerop 0.0)",
13        "(eq 'a 'a)", "(equal '(1 \"a\") (list 1 \"a\"))", "(null nil)", "(not 0)", "(car '(1 2))", "(cdr '(1 2))",
14        "(cdr '(1 . 2))", "(cons 1 2)", "(cons 1 '(2))", "(nth 2 '(a b c))", "(nth 5 '(a))", "(length \"héllo\")",
15        "(reverse '(1 2 3))", "(append '(1) '(2) 3)", "(number-sequence 1 5 2)", "(memq 'b '(a b c))", "(member \"b\" '(\"a\" \"b\"))",
16        "(assoc \"k\" '((\"k\" . 1)))", "(mapcar #'1+ '(1 2 3))", "(mapconcat #'identity '(\"a\" \"b\") \"-\")",
17        "(apply #'+ 1 '(2 3))", "(funcall (lambda (x &optional y) (list x y)) 1)", "(let ((x 1) (y 2)) (+ x y))",
18        "(let* ((x 1) (y (1+ x))) y)", "(let ((n 0)) (dotimes (i 4) (setq n (+ n i))) n)", "(let (r) (dolist (x '(1 2)) (push x r)) r)",
19        "(cond ((= 1 2) 'a) ((= 1 1) 'b))", "(and 1 2)", "(or nil 3)", "(if nil 1 2 3)", "(when t 1 2)", "(unless t 1)",
20        "(concat \"a\" \"b\" '(99))", "(format \"%d|%5.2f|%s|%S|%-4s|%x|%c|%%\" 42 3.14159 \"s\" \"q\" \"ab\" 255 65)",
21        "(format \"%s %s\" 1.0 100.5)", "(format \"%d\" 2.9)", "(format \"%03d\" 7)", "(format \"%e\" 12345.678)", "(format \"%g\" 0.0001)",
22        "(substring \"hello\" 1 3)", "(substring \"hello\" -3)", "(string-to-number \" 12abc\")", "(string-to-number \"1.5e2\")",
23        "(string-to-number \"3.\")", "(string-to-number \"x\")", "(number-to-string 0.1)", "(number-to-string 1e21)", "(upcase \"ab\")",
24        "(string= \"a\" \"a\")", "(string< \"a\" \"b\")", "(split-string \" a  b \")", "(split-string \"a,b,,c\" \",\")",
25        "(string-prefix-p \"ab\" \"abc\")", "(condition-case nil (/ 1 0) (error 'caught))", "(ignore-errors (car 1))", "(car 1)",
26        "(let ((x 0)) (while (< x 5) (setq x (1+ x))) x)", "(elt [1 2 3] 1)", "(aref \"abc\" 1)", "(length [1 2])", "(nthcdr 1 '(1 2 3))",
27        "(delq nil '(1 nil 2))", "(cadr '(1 2 3))", "(identity \"x\")", "(capitalize \"hello world\")", "(string-trim \"  x \")",
28    ]
29
30    static func ours(_ form: String) -> String {
31        guard let sexp = try? LispReader.readFirst(form).sexp else { return "read-error" }
32        do {
33            return Elisp.printed(try Elisp().eval(sexp), escape: true)
34        } catch {
35            return "error"
36        }
37    }
38
39    @Test(.enabled(if: oracle))
40    func matchesEmacs() throws {
41        let list = "(list " + Self.forms.map { "(condition-case nil (prin1-to-string \($0)) (error \"error\"))" }.joined(separator: " ") + ")"
42        let emacs = try EmacsOracle.evaluate("", "(let ((print-escape-newlines t)) \(list))")
43        #expect(emacs.count == Self.forms.count)
44        for (form, expected) in zip(Self.forms, emacs) {
45            #expect(Self.ours(form) == expected, "\(form)")
46        }
47    }
48}
49
50/// Inputs that once crashed the evaluator, the diary functions or duration formulas.
51struct HostileInputTests {
52    static let forms = [
53        "(1+ 9223372036854775807)", "(1- -9223372036854775808)", "(- -9223372036854775808)", "(abs -9223372036854775808)",
54        "(% -9223372036854775808 -1)", "(mod -9223372036854775808 -1)", "(/ -9223372036854775808 -1)",
55        "(upcase -1)", "(downcase -1)", "(concat (list -1))", "(format \"%c\" -1)", "(format \"%c\" 99999999)",
56        "(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))",
57        "(dotimes (i 10000000000) nil)", "(number-sequence 1 10000000000)",
58    ]
59
60    @Test func evaluatorRefusesWithoutCrashing() {
61        for form in Self.forms { #expect(ElispTests.ours(form) == "error", "\(form)") }
62    }
63
64    /// On a stack the size of a background task's, deep recursion stops with an error.
65    @Test func deepRecursionFitsABackgroundStack() async {
66        let done = await withCheckedContinuation { continuation in
67            let thread = Thread {
68                continuation.resume(returning: ElispTests.ours("(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))"))
69            }
70            thread.stackSize = 512 * 1024
71            thread.start()
72        }
73        #expect(done == "error")
74    }
75
76    @Test func diaryFunctionsRefuseWithoutCrashing() throws {
77        let day = Days.absolute(year: 2026, month: 10, day: 7)
78        for form in ["(calendar-last-day-of-month -20 2024)", "(calendar-gregorian-from-absolute 9223372036854775807)",
79                     "(calendar-nth-named-absday -9223372036854775808 1 10 2026)", "(diary-remind '(diary-date 1 1 2027) -9223372036854775808)",
80                     "(diary-float 10 3 -9223372036854775808)", "(diary-anniversary 10 7 -9223372036854775808)"] {
81            #expect(DiarySexp.entries(try LispReader.readFirst(form).sexp, entry: "E", day: day) == nil, "\(form)")
82        }
83    }
84
85    @Test func durationsAndHugeNumbersDoNotCrashFormulas() {
86        for table in [
87            "| 1e300 | |\n#+TBLFM: $2=$1*2;T\n", "| 1e300 | |\n#+TBLFM: $2='(* $1 2);T\n", "| 9999999999999999:00:00 | |\n#+TBLFM: $2=$1;T\n",
88            "| 1 | |\n#+TBLFM: $2=$1;p99999999999999999999\n", "| 1 | |\n#+TBLFM: $2=$99999999999999999999\n",
89            "| 1 | |\n#+TBLFM: @99999999999999999999$2=1\n", "#+CONSTANTS: a=$a\n| 1 | |\n#+TBLFM: $2=$a\n",
90        ] {
91            let caret = (table as NSString).range(of: "#+TBLFM").location + 3
92            let result = runCommand(TableRecalculate(all: true), table, caret: caret)
93            #expect(result.text == table, "\(table)")
94        }
95    }
96}