aboutsummaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync-async-worker.el
blob: 3bf803ec5057638c3ca4b17a06e03fd633ce8cad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
;;; 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/" 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--worker-command-puts-L-before-l ()
  "Order: `-L module-dir' must precede `-l calendar-sync.el' so the sibling
load-path entry is in effect by the time the module load runs.  Asserting both
via `member' separately wouldn't catch a swap."
  (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 (< (cl-position "-L" command :test #'equal)
               (cl-position "-l" command :test #'equal)))))

(ert-deftest test-calendar-sync--worker-command-loads-sibling-modules-without-init ()
  "The worker command should load calendar-sync and sibling modules without init."
  :tags '(:slow)
  (let* ((calendar-sync--module-file
          (expand-file-name "modules/calendar-sync.el" user-emacs-directory))
         (command (append
                   (calendar-sync--worker-command "/tmp/input.ics" "/tmp/output.org")
                   (list "--eval" "(princ \"loaded\")"))))
    ;; Replace the conversion eval with a harmless smoke expression so this
    ;; test exercises load-path setup without requiring temp ICS input.
    (setq command
          (cl-loop for arg in command
                   collect (if (and (stringp arg)
                                    (string-match-p "calendar-sync--batch-convert-file" arg))
                               "(princ \"\")"
                             arg)))
    (with-temp-buffer
      (let ((exit-code (apply #'call-process
                              (car command) nil t nil (cdr command))))
        (should (= 0 exit-code))
        (should (string-match-p "loaded" (buffer-string)))))))

(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