aboutsummaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync-async-worker.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-10 03:09:37 -0500
committerCraig Jennings <c@cjennings.net>2026-05-10 03:09:37 -0500
commitf89b6f22409318ac3124138f7d230c829e6d73c5 (patch)
tree85bef5f0016fa66b082e524e0f152e844a60a8c3 /tests/test-calendar-sync-async-worker.el
parent1d24227c1335b8b154d0bd14a34b6db5e8069f02 (diff)
downloaddotemacs-f89b6f22409318ac3124138f7d230c829e6d73c5.tar.gz
dotemacs-f89b6f22409318ac3124138f7d230c829e6d73c5.zip
Keep calendar sync off the UI thread
Move calendar feed conversion into an isolated batch Emacs worker so large parse/write cycles do not freeze interactive editing. Cover the worker command, isolated logging, quoted settings, and sync success/failure paths with focused ERTs.
Diffstat (limited to 'tests/test-calendar-sync-async-worker.el')
-rw-r--r--tests/test-calendar-sync-async-worker.el154
1 files changed, 154 insertions, 0 deletions
diff --git a/tests/test-calendar-sync-async-worker.el b/tests/test-calendar-sync-async-worker.el
new file mode 100644
index 00000000..d5982d32
--- /dev/null
+++ b/tests/test-calendar-sync-async-worker.el
@@ -0,0 +1,154 @@
+;;; test-calendar-sync-async-worker.el --- Tests for async calendar conversion -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Regression tests for keeping calendar sync parse/write work off the main
+;; Emacs thread.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'testutil-calendar-sync)
+(require 'calendar-sync)
+
+(ert-deftest test-calendar-sync--worker-command-loads-module-without-init ()
+ "The conversion worker should run batch Emacs without user init."
+ (let* ((calendar-sync--module-file "/tmp/calendar-sync.el")
+ (calendar-sync-past-months 2)
+ (calendar-sync-future-months 6)
+ (calendar-sync-user-emails '("me@example.test"))
+ (command (calendar-sync--worker-command "/tmp/input.ics" "/tmp/output.org")))
+ (should (member "--batch" command))
+ (should (member "--no-site-file" command))
+ (should (member "--no-site-lisp" command))
+ (should (member "-l" command))
+ (should (member "/tmp/calendar-sync.el" command))
+ (should (cl-some (lambda (arg)
+ (and (stringp arg)
+ (string-match-p "calendar-sync-auto-start nil" arg)))
+ command))
+ (should (cl-some (lambda (arg)
+ (and (stringp arg)
+ (string-match-p "calendar-sync--batch-convert-file" arg)
+ (string-match-p "/tmp/input\\.ics" arg)
+ (string-match-p "/tmp/output\\.org" arg)
+ (string-match-p "'(\"me@example\\.test\")" arg)))
+ command))))
+
+(ert-deftest test-calendar-sync--batch-convert-file-writes-org-output ()
+ "The worker entry point should convert an ICS file and write Org output."
+ (let* ((input-file (make-temp-file "calendar-sync-worker-" nil ".ics"))
+ (output-file (make-temp-file "calendar-sync-worker-" nil ".org"))
+ (event (test-calendar-sync-make-vevent
+ "Worker Meeting"
+ (test-calendar-sync-time-tomorrow-at 9 0)
+ (test-calendar-sync-time-tomorrow-at 10 0)))
+ (ics (test-calendar-sync-make-ics event)))
+ (unwind-protect
+ (progn
+ (with-temp-file input-file
+ (insert ics))
+ (delete-file output-file)
+ (calendar-sync--batch-convert-file input-file output-file 3 12 '("me@example.test"))
+ (should (file-exists-p output-file))
+ (with-temp-buffer
+ (insert-file-contents output-file)
+ (should (string-match-p "\\* Worker Meeting" (buffer-string)))))
+ (when (file-exists-p input-file)
+ (delete-file input-file))
+ (when (file-exists-p output-file)
+ (delete-file output-file)))))
+
+(ert-deftest test-calendar-sync--parse-ics-does-not-require-cj-log-silently ()
+ "Worker parsing should not fail when the rest of the config is not loaded."
+ (let ((original-log-function
+ (when (fboundp 'cj/log-silently)
+ (symbol-function 'cj/log-silently))))
+ (unwind-protect
+ (progn
+ (when (fboundp 'cj/log-silently)
+ (fmakunbound 'cj/log-silently))
+ (should-not (calendar-sync--parse-ics "not valid ics")))
+ (when original-log-function
+ (fset 'cj/log-silently original-log-function)))))
+
+(ert-deftest test-calendar-sync--sync-calendar-uses-worker-for-parse-and-write ()
+ "Sync should fetch to a file and hand parse/write work to a worker process."
+ (let ((calendar '(:name "work"
+ :url "https://example.test/work.ics"
+ :file "/tmp/work.org"))
+ (calendar-sync--calendar-states (make-hash-table :test 'equal))
+ (fetched-url nil)
+ (worker-input nil)
+ (worker-output nil)
+ (saved-state nil))
+ (cl-letf (((symbol-function 'calendar-sync--fetch-ics-file)
+ (lambda (url callback)
+ (setq fetched-url url)
+ (funcall callback "/tmp/work.ics")))
+ ((symbol-function 'calendar-sync--convert-ics-file-async)
+ (lambda (ics-file output-file callback)
+ (setq worker-input ics-file
+ worker-output output-file)
+ (funcall callback t "")))
+ ((symbol-function 'calendar-sync--parse-ics)
+ (lambda (&rest _args)
+ (ert-fail "sync-calendar parsed ICS on the main thread")))
+ ((symbol-function 'calendar-sync--write-file)
+ (lambda (&rest _args)
+ (ert-fail "sync-calendar wrote the Org file on the main thread")))
+ ((symbol-function 'calendar-sync--save-state)
+ (lambda ()
+ (setq saved-state t)))
+ ((symbol-function 'message) (lambda (&rest _args) nil)))
+ (calendar-sync--sync-calendar calendar))
+ (should (string= "https://example.test/work.ics" fetched-url))
+ (should (string= "/tmp/work.ics" worker-input))
+ (should (string= "/tmp/work.org" worker-output))
+ (should saved-state)
+ (should (eq 'ok (plist-get (calendar-sync--get-calendar-state "work") :status)))))
+
+(ert-deftest test-calendar-sync--sync-calendar-records-worker-failure ()
+ "Worker conversion failures should be reflected in calendar state."
+ (let ((calendar '(:name "work"
+ :url "https://example.test/work.ics"
+ :file "/tmp/work.org"))
+ (calendar-sync--calendar-states (make-hash-table :test 'equal))
+ (saved-state nil))
+ (cl-letf (((symbol-function 'calendar-sync--fetch-ics-file)
+ (lambda (_url callback)
+ (funcall callback "/tmp/work.ics")))
+ ((symbol-function 'calendar-sync--convert-ics-file-async)
+ (lambda (_ics-file _output-file callback)
+ (funcall callback nil "parse failed")))
+ ((symbol-function 'calendar-sync--save-state)
+ (lambda ()
+ (setq saved-state t)))
+ ((symbol-function 'message) (lambda (&rest _args) nil)))
+ (calendar-sync--sync-calendar calendar))
+ (let ((state (calendar-sync--get-calendar-state "work")))
+ (should saved-state)
+ (should (eq 'error (plist-get state :status)))
+ (should (string-match-p "parse failed" (plist-get state :last-error))))))
+
+(ert-deftest test-calendar-sync--sync-calendar-handles-empty-worker-error ()
+ "Worker failures without stderr should still produce a useful state error."
+ (let ((calendar '(:name "work"
+ :url "https://example.test/work.ics"
+ :file "/tmp/work.org"))
+ (calendar-sync--calendar-states (make-hash-table :test 'equal)))
+ (cl-letf (((symbol-function 'calendar-sync--fetch-ics-file)
+ (lambda (_url callback)
+ (funcall callback "/tmp/work.ics")))
+ ((symbol-function 'calendar-sync--convert-ics-file-async)
+ (lambda (_ics-file _output-file callback)
+ (funcall callback nil nil)))
+ ((symbol-function 'calendar-sync--save-state) (lambda () nil))
+ ((symbol-function 'message) (lambda (&rest _args) nil)))
+ (calendar-sync--sync-calendar calendar))
+ (let ((state (calendar-sync--get-calendar-state "work")))
+ (should (eq 'error (plist-get state :status)))
+ (should (string= "Conversion failed" (plist-get state :last-error))))))
+
+(provide 'test-calendar-sync-async-worker)
+;;; test-calendar-sync-async-worker.el ends here