diff options
| author | Craig Jennings <c@cjennings.net> | 2025-12-02 07:55:21 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-12-02 07:55:21 -0600 |
| commit | fac78a8fc92b27c37e678ee70824eb5f70ceee8b (patch) | |
| tree | 570e67b7915d50c0831c3ce74ddfee873151b6d0 /tests | |
| parent | 031b55dc59717dc437c9e46376c56344848c863f (diff) | |
Added multi-URL calendar sync supporting Google and Proton calendars.
Each calendar syncs to separate file with per-calendar state tracking.
Added 13 property-based tests for RRULE expansion. Total: 150 tests passing.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-calendar-sync--expand-weekly.el | 6 | ||||
| -rw-r--r-- | tests/test-calendar-sync-properties.el | 239 | ||||
| -rw-r--r-- | tests/test-calendar-sync.el | 79 | ||||
| -rw-r--r-- | tests/test-integration-recurring-events.el | 2 | ||||
| -rw-r--r-- | tests/testutil-calendar-sync.el | 90 |
5 files changed, 398 insertions, 18 deletions
diff --git a/tests/test-calendar-sync--expand-weekly.el b/tests/test-calendar-sync--expand-weekly.el index e4e5b738..fe333c98 100644 --- a/tests/test-calendar-sync--expand-weekly.el +++ b/tests/test-calendar-sync--expand-weekly.el @@ -122,7 +122,8 @@ (unwind-protect (let* ((start-date (test-calendar-sync-time-days-from-now 1 10 0)) (end-date (test-calendar-sync-time-days-from-now 1 11 0)) - (until-date (test-calendar-sync-time-days-from-now 60 0 0)) + ;; UNTIL must be date-only (3 elements) for calendar-sync--before-date-p + (until-date (test-calendar-sync-time-date-only 60)) (base-event (list :summary "Time-Limited Event" :start start-date :end end-date)) @@ -253,7 +254,8 @@ (unwind-protect (let* ((start-date (test-calendar-sync-time-days-ago 100 10 0)) (end-date (test-calendar-sync-time-days-ago 100 11 0)) - (until-date (test-calendar-sync-time-days-ago 50 0 0)) + ;; UNTIL must be date-only (3 elements) for calendar-sync--before-date-p + (until-date (test-calendar-sync-time-date-only-ago 50)) (base-event (list :summary "Past Event" :start start-date :end end-date)) diff --git a/tests/test-calendar-sync-properties.el b/tests/test-calendar-sync-properties.el new file mode 100644 index 00000000..6054fc5e --- /dev/null +++ b/tests/test-calendar-sync-properties.el @@ -0,0 +1,239 @@ +;;; test-calendar-sync-properties.el --- Property-based tests for calendar-sync -*- lexical-binding: t; -*- + +;;; Commentary: +;; Property-based tests for RRULE expansion functions. +;; These tests verify invariants hold across randomly generated inputs, +;; complementing the example-based tests in other test files. +;; +;; Each test runs multiple trials with random parameters to explore +;; the input space and find edge cases that example-based tests miss. +;; +;; Properties tested: +;; 1. COUNT always limits total occurrences +;; 2. UNTIL date bounds all occurrences +;; 3. BYDAY constrains weekly occurrences to specified weekdays +;; 4. INTERVAL creates correct spacing between occurrences +;; 5. All occurrences fall within the date range +;; 6. Expansion is deterministic (same inputs → same outputs) + +;;; Code: + +(require 'ert) +(require 'calendar-sync) +(require 'testutil-calendar-sync) + +(defconst test-calendar-sync-property-trials 30 + "Number of random trials to run for each property test. +Higher values give more confidence but slower tests.") + +;;; Property 1: COUNT Ceiling + +(ert-deftest test-calendar-sync-property-count-limits-daily () + "Property: COUNT parameter limits daily occurrences. +For any COUNT value N, expansion never produces more than N occurrences." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((count (1+ (random 20))) + (start-date (test-calendar-sync-random-future-date)) + (base-event (list :summary "Daily Test" :start start-date)) + (rrule (list :freq 'daily :interval 1 :count count)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-daily base-event rrule range))) + (should (<= (length occurrences) count))))) + +(ert-deftest test-calendar-sync-property-count-limits-weekly () + "Property: COUNT parameter limits weekly occurrences. +For any COUNT value N, expansion never produces more than N occurrences." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((count (1+ (random 20))) + (weekdays (test-calendar-sync-random-weekday-subset)) + (start-date (test-calendar-sync-random-future-date)) + (base-event (list :summary "Weekly Test" :start start-date)) + (rrule (list :freq 'weekly :byday weekdays :interval 1 :count count)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-weekly base-event rrule range))) + (should (<= (length occurrences) count))))) + +(ert-deftest test-calendar-sync-property-count-limits-monthly () + "Property: COUNT parameter limits monthly occurrences." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((count (1+ (random 15))) + (start-date (test-calendar-sync-random-future-date)) + (base-event (list :summary "Monthly Test" :start start-date)) + (rrule (list :freq 'monthly :interval 1 :count count)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-monthly base-event rrule range))) + (should (<= (length occurrences) count))))) + +(ert-deftest test-calendar-sync-property-count-limits-yearly () + "Property: COUNT parameter limits yearly occurrences." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((count (1+ (random 5))) + (start-date (test-calendar-sync-random-future-date)) + (base-event (list :summary "Yearly Test" :start start-date)) + (rrule (list :freq 'yearly :interval 1 :count count)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-yearly base-event rrule range))) + (should (<= (length occurrences) count))))) + +;;; Property 2: UNTIL Boundary + +(ert-deftest test-calendar-sync-property-until-bounds-daily () + "Property: No daily occurrence starts on or after UNTIL date." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (until-days (+ 10 (random 60))) + ;; UNTIL must be date-only (3 elements) for calendar-sync--before-date-p + (until-date (test-calendar-sync-time-date-only until-days)) + (base-event (list :summary "Until Test" :start start-date)) + (rrule (list :freq 'daily :interval 1 :until until-date)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-daily base-event rrule range))) + (dolist (occ occurrences) + (let ((occ-start (plist-get occ :start))) + (should (calendar-sync--before-date-p + (list (nth 0 occ-start) (nth 1 occ-start) (nth 2 occ-start)) + until-date))))))) + +(ert-deftest test-calendar-sync-property-until-bounds-weekly () + "Property: No weekly occurrence starts on or after UNTIL date." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (until-days (+ 14 (random 60))) + ;; UNTIL must be date-only (3 elements) for calendar-sync--before-date-p + (until-date (test-calendar-sync-time-date-only until-days)) + (weekdays (test-calendar-sync-random-weekday-subset)) + (base-event (list :summary "Until Test" :start start-date)) + (rrule (list :freq 'weekly :byday weekdays :interval 1 :until until-date)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-weekly base-event rrule range))) + (dolist (occ occurrences) + (let ((occ-start (plist-get occ :start))) + (should (calendar-sync--before-date-p + (list (nth 0 occ-start) (nth 1 occ-start) (nth 2 occ-start)) + until-date))))))) + +;;; Property 3: BYDAY Constraint + +(ert-deftest test-calendar-sync-property-byday-constrains-weekdays () + "Property: Weekly occurrences only fall on BYDAY weekdays. +Every generated occurrence must be on one of the specified weekdays." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((weekdays (test-calendar-sync-random-weekday-subset)) + (weekday-nums (mapcar #'calendar-sync--weekday-to-number weekdays)) + (start-date (test-calendar-sync-random-future-date)) + (base-event (list :summary "BYDAY Test" :start start-date)) + (rrule (list :freq 'weekly :byday weekdays :interval 1)) + (range (test-calendar-sync-narrow-range)) + (occurrences (calendar-sync--expand-weekly base-event rrule range))) + (dolist (occ occurrences) + (let* ((occ-start (plist-get occ :start)) + (occ-weekday (calendar-sync--date-weekday + (list (nth 0 occ-start) (nth 1 occ-start) (nth 2 occ-start))))) + (should (member occ-weekday weekday-nums))))))) + +;;; Property 4: INTERVAL Spacing + +(ert-deftest test-calendar-sync-property-interval-spacing-daily () + "Property: Daily occurrences are spaced INTERVAL days apart. +Consecutive occurrences should be exactly INTERVAL days apart." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((interval (1+ (random 5))) + (start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (base-event (list :summary "Interval Test" :start start-date)) + (rrule (list :freq 'daily :interval interval :count 10)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-daily base-event rrule range))) + (when (> (length occurrences) 1) + (let ((dates (mapcar (lambda (o) (plist-get o :start)) occurrences))) + (cl-loop for i from 0 below (1- (length dates)) + for d1 = (nth i dates) + for d2 = (nth (1+ i) dates) + do (let ((gap (round (test-calendar-sync-days-between d1 d2)))) + (should (= interval gap))))))))) + +(ert-deftest test-calendar-sync-property-interval-spacing-weekly-single-day () + "Property: Weekly single-day occurrences are spaced INTERVAL weeks apart." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((interval (1+ (random 3))) + (weekday (nth (random 7) '("MO" "TU" "WE" "TH" "FR" "SA" "SU"))) + (start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (base-event (list :summary "Weekly Interval Test" :start start-date)) + (rrule (list :freq 'weekly :byday (list weekday) :interval interval :count 8)) + (range (test-calendar-sync-wide-range)) + (occurrences (calendar-sync--expand-weekly base-event rrule range))) + (when (> (length occurrences) 1) + (let ((dates (mapcar (lambda (o) (plist-get o :start)) occurrences))) + (cl-loop for i from 0 below (1- (length dates)) + for d1 = (nth i dates) + for d2 = (nth (1+ i) dates) + do (let ((gap (round (test-calendar-sync-days-between d1 d2)))) + (should (= (* 7 interval) gap))))))))) + +;;; Property 5: Range Containment + +(ert-deftest test-calendar-sync-property-occurrences-within-range () + "Property: All occurrences fall within the date range. +No occurrence should be before range start or after range end." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((range-start-days (random 30)) + (range-end-days (+ range-start-days 30 (random 60))) + (range (list (time-add (current-time) (* range-start-days 86400)) + (time-add (current-time) (* range-end-days 86400)))) + (start-date (test-calendar-sync-time-days-from-now (1+ range-start-days) 10 0)) + (base-event (list :summary "Range Test" :start start-date)) + (rrule (list :freq 'daily :interval 1)) + (occurrences (calendar-sync--expand-daily base-event rrule range))) + (dolist (occ occurrences) + (let ((occ-start (plist-get occ :start))) + (should (calendar-sync--date-in-range-p occ-start range))))))) + +(ert-deftest test-calendar-sync-property-weekly-occurrences-within-range () + "Property: All weekly occurrences fall within the date range." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((range (test-calendar-sync-narrow-range)) + (start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (weekdays (test-calendar-sync-random-weekday-subset)) + (base-event (list :summary "Range Test" :start start-date)) + (rrule (list :freq 'weekly :byday weekdays :interval 1)) + (occurrences (calendar-sync--expand-weekly base-event rrule range))) + (dolist (occ occurrences) + (let ((occ-start (plist-get occ :start))) + (should (calendar-sync--date-in-range-p occ-start range))))))) + +;;; Property 6: Determinism + +(ert-deftest test-calendar-sync-property-expansion-deterministic-daily () + "Property: Same inputs produce identical outputs for daily expansion." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((interval (1+ (random 3))) + (count (+ 5 (random 10))) + (start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (base-event (list :summary "Determinism Test" :start start-date)) + (rrule (list :freq 'daily :interval interval :count count)) + (range (test-calendar-sync-wide-range)) + (result1 (calendar-sync--expand-daily base-event rrule range)) + (result2 (calendar-sync--expand-daily base-event rrule range))) + (should (= (length result1) (length result2))) + (cl-loop for o1 in result1 + for o2 in result2 + do (should (equal (plist-get o1 :start) (plist-get o2 :start))))))) + +(ert-deftest test-calendar-sync-property-expansion-deterministic-weekly () + "Property: Same inputs produce identical outputs for weekly expansion." + (dotimes (_ test-calendar-sync-property-trials) + (let* ((interval (1+ (random 2))) + (weekdays (test-calendar-sync-random-weekday-subset)) + (count (+ 5 (random 10))) + (start-date (test-calendar-sync-time-days-from-now 1 10 0)) + (base-event (list :summary "Determinism Test" :start start-date)) + (rrule (list :freq 'weekly :byday weekdays :interval interval :count count)) + (range (test-calendar-sync-wide-range)) + (result1 (calendar-sync--expand-weekly base-event rrule range)) + (result2 (calendar-sync--expand-weekly base-event rrule range))) + (should (= (length result1) (length result2))) + (cl-loop for o1 in result1 + for o2 in result2 + do (should (equal (plist-get o1 :start) (plist-get o2 :start))))))) + +(provide 'test-calendar-sync-properties) +;;; test-calendar-sync-properties.el ends here diff --git a/tests/test-calendar-sync.el b/tests/test-calendar-sync.el index 2de306b1..7cda5e73 100644 --- a/tests/test-calendar-sync.el +++ b/tests/test-calendar-sync.el @@ -452,7 +452,7 @@ Earlier events should appear first in the output." (ics (test-calendar-sync-make-ics event)) (org-content (calendar-sync--parse-ics ics))) (should org-content) - (should (string-match-p "^# Google Calendar Events" org-content)) + (should (string-match-p "^# Calendar Events" org-content)) (should (string-match-p "\\* Test Event" org-content)))) (ert-deftest test-calendar-sync--parse-ics-normal-multiple-events-all-included () @@ -577,7 +577,11 @@ Valid events should be parsed, invalid ones skipped." (original-offset -21600) ; CST (original-time (current-time)) (calendar-sync--last-timezone-offset original-offset) - (calendar-sync--last-sync-time original-time)) + (calendar-sync--calendar-states (make-hash-table :test 'equal))) + ;; Set up per-calendar state + (puthash "test-calendar" + (list :status 'ok :last-sync original-time :last-error nil) + calendar-sync--calendar-states) (unwind-protect (progn ;; Save state @@ -586,14 +590,17 @@ Valid events should be parsed, invalid ones skipped." ;; Clear variables (setq calendar-sync--last-timezone-offset nil) - (setq calendar-sync--last-sync-time nil) + (clrhash calendar-sync--calendar-states) ;; Load state (calendar-sync--load-state) ;; Verify loaded correctly (should (= original-offset calendar-sync--last-timezone-offset)) - (should (equal original-time calendar-sync--last-sync-time))) + (let ((loaded-state (gethash "test-calendar" calendar-sync--calendar-states))) + (should loaded-state) + (should (eq 'ok (plist-get loaded-state :status))) + (should (equal original-time (plist-get loaded-state :last-sync))))) ;; Cleanup (when (file-exists-p test-state-file) (delete-file test-state-file))))) @@ -604,7 +611,7 @@ Valid events should be parsed, invalid ones skipped." (test-state-file (expand-file-name "subdir/state.el" test-dir)) (calendar-sync--state-file test-state-file) (calendar-sync--last-timezone-offset -21600) - (calendar-sync--last-sync-time (current-time))) + (calendar-sync--calendar-states (make-hash-table :test 'equal))) (unwind-protect (progn (calendar-sync--save-state) @@ -618,35 +625,79 @@ Valid events should be parsed, invalid ones skipped." "Test that load-state handles missing file gracefully." (let ((calendar-sync--state-file "/nonexistent/path/state.el") (calendar-sync--last-timezone-offset nil) - (calendar-sync--last-sync-time nil)) + (calendar-sync--calendar-states (make-hash-table :test 'equal))) ;; Should not error (should-not (calendar-sync--load-state)) - ;; Variables should remain nil + ;; Variables should remain nil/empty (should-not calendar-sync--last-timezone-offset) - (should-not calendar-sync--last-sync-time))) + (should (= 0 (hash-table-count calendar-sync--calendar-states))))) (ert-deftest test-calendar-sync--load-state-handles-corrupted-file () "Test that load-state handles corrupted state file gracefully." (let* ((test-state-file (make-temp-file "calendar-sync-test-state")) (calendar-sync--state-file test-state-file) (calendar-sync--last-timezone-offset nil) - (calendar-sync--last-sync-time nil)) + (calendar-sync--calendar-states (make-hash-table :test 'equal))) (unwind-protect (progn ;; Write corrupted data (with-temp-file test-state-file (insert "this is not valid elisp {[}")) - ;; Should handle error gracefully (catches error, logs message) - ;; Returns error message string, not nil, but doesn't throw - (should (stringp (calendar-sync--load-state))) + ;; Should handle error gracefully (catches error, logs message, returns nil) + ;; Function logs to *Messages* but returns nil (doesn't crash) + (should-not (calendar-sync--load-state)) - ;; Variables should remain nil (not loaded from corrupted file) + ;; Variables should remain nil/empty (not loaded from corrupted file) (should-not calendar-sync--last-timezone-offset) - (should-not calendar-sync--last-sync-time)) + (should (= 0 (hash-table-count calendar-sync--calendar-states)))) ;; Cleanup (when (file-exists-p test-state-file) (delete-file test-state-file))))) +;;; Tests: Multi-Calendar Configuration + +(ert-deftest test-calendar-sync--calendar-names-returns-names () + "Test that calendar-names returns list of calendar names." + (let ((calendar-sync-calendars + '((:name "cal1" :url "http://example.com/1" :file "/tmp/cal1.org") + (:name "cal2" :url "http://example.com/2" :file "/tmp/cal2.org")))) + (should (equal '("cal1" "cal2") (calendar-sync--calendar-names))))) + +(ert-deftest test-calendar-sync--calendar-names-empty-when-no-calendars () + "Test that calendar-names returns empty list when no calendars configured." + (let ((calendar-sync-calendars nil)) + (should (null (calendar-sync--calendar-names))))) + +(ert-deftest test-calendar-sync--get-calendar-by-name-finds-calendar () + "Test that get-calendar-by-name finds correct calendar." + (let ((calendar-sync-calendars + '((:name "google" :url "http://google.com" :file "/tmp/gcal.org") + (:name "proton" :url "http://proton.me" :file "/tmp/pcal.org")))) + (let ((found (calendar-sync--get-calendar-by-name "proton"))) + (should found) + (should (string= "proton" (plist-get found :name))) + (should (string= "http://proton.me" (plist-get found :url)))))) + +(ert-deftest test-calendar-sync--get-calendar-by-name-returns-nil-for-unknown () + "Test that get-calendar-by-name returns nil for unknown calendar." + (let ((calendar-sync-calendars + '((:name "google" :url "http://google.com" :file "/tmp/gcal.org")))) + (should (null (calendar-sync--get-calendar-by-name "nonexistent"))))) + +(ert-deftest test-calendar-sync--get-calendar-state-returns-nil-for-new () + "Test that get-calendar-state returns nil for calendar without state." + (let ((calendar-sync--calendar-states (make-hash-table :test 'equal))) + (should (null (calendar-sync--get-calendar-state "new-calendar"))))) + +(ert-deftest test-calendar-sync--set-and-get-calendar-state () + "Test setting and getting calendar state." + (let ((calendar-sync--calendar-states (make-hash-table :test 'equal)) + (test-state '(:status ok :last-sync (0 0 0) :last-error nil))) + (calendar-sync--set-calendar-state "test-cal" test-state) + (let ((retrieved (calendar-sync--get-calendar-state "test-cal"))) + (should retrieved) + (should (eq 'ok (plist-get retrieved :status)))))) + (provide 'test-calendar-sync) ;;; test-calendar-sync.el ends here diff --git a/tests/test-integration-recurring-events.el b/tests/test-integration-recurring-events.el index 0d32d9e0..4629e6ef 100644 --- a/tests/test-integration-recurring-events.el +++ b/tests/test-integration-recurring-events.el @@ -105,7 +105,7 @@ Validates: (let ((org-output (calendar-sync--parse-ics test-integration-recurring-events--weekly-ics))) ;; Should generate org-formatted output (should (stringp org-output)) - (should (string-match-p "^# Google Calendar Events" org-output)) + (should (string-match-p "^# Calendar Events" org-output)) ;; Should contain multiple GTFO entries (let ((gtfo-count (with-temp-buffer diff --git a/tests/testutil-calendar-sync.el b/tests/testutil-calendar-sync.el index c83006b5..d1a94b01 100644 --- a/tests/testutil-calendar-sync.el +++ b/tests/testutil-calendar-sync.el @@ -8,6 +8,15 @@ (require 'calendar) +;;; Test Environment Setup + +;; Provide stub for cj/log-silently if not already defined +;; This function is defined in system-lib.el but tests should run standalone +(unless (fboundp 'cj/log-silently) + (defun cj/log-silently (format-string &rest args) + "Stub for testing: silently ignore log messages." + nil)) + ;;; Dynamic Timestamp Generation (defun test-calendar-sync-time-today-at (hour minute) @@ -48,7 +57,7 @@ Returns (year month day hour minute) list suitable for tests." (defun test-calendar-sync-time-date-only (offset-days) "Generate date-only timestamp for OFFSET-DAYS from now. -Returns (year month day) list for all-day events." +Returns (year month day) list for all-day events and UNTIL dates." (let* ((future (time-add (current-time) (* offset-days 24 3600))) (decoded (decode-time future)) (year (nth 5 decoded)) @@ -56,6 +65,22 @@ Returns (year month day) list for all-day events." (day (nth 3 decoded))) (list year month day))) +(defun test-calendar-sync-time-date-only-ago (offset-days) + "Generate date-only timestamp for OFFSET-DAYS ago. +Returns (year month day) list for UNTIL dates in the past." + (let* ((past (time-subtract (current-time) (* offset-days 24 3600))) + (decoded (decode-time past)) + (year (nth 5 decoded)) + (month (nth 4 decoded)) + (day (nth 3 decoded))) + (list year month day))) + +(defun test-calendar-sync-date-only-from-datetime (datetime) + "Extract date-only (year month day) from DATETIME list. +DATETIME is (year month day hour minute). +Returns (year month day) suitable for UNTIL dates." + (list (nth 0 datetime) (nth 1 datetime) (nth 2 datetime))) + ;;; .ics Test Data Generation (defun test-calendar-sync-ics-datetime (time-list) @@ -106,5 +131,68 @@ Each event should be a VEVENT string from `test-calendar-sync-make-vevent'." (string-join events "\n") "\nEND:VCALENDAR")) +;;; Property Test Helpers + +(defun test-calendar-sync-random-future-date () + "Generate random date 1-180 days in future with random time. +Returns (year month day hour minute) list." + (test-calendar-sync-time-days-from-now + (1+ (random 180)) + (random 24) + (random 60))) + +(defun test-calendar-sync-random-past-date () + "Generate random date 1-90 days in past with random time. +Returns (year month day hour minute) list." + (test-calendar-sync-time-days-ago + (1+ (random 90)) + (random 24) + (random 60))) + +(defun test-calendar-sync-random-weekday-subset () + "Generate random non-empty subset of weekdays. +Returns list of weekday strings like (\"MO\" \"WE\" \"FR\")." + (let ((days '("MO" "TU" "WE" "TH" "FR" "SA" "SU")) + (result '())) + (dolist (day days) + (when (zerop (random 2)) + (push day result))) + ;; Ensure non-empty + (or result (list (nth (random 7) days))))) + +(defun test-calendar-sync-random-freq () + "Return random RRULE frequency symbol." + (nth (random 4) '(daily weekly monthly yearly))) + +(defun test-calendar-sync-days-between (date1 date2) + "Calculate days between DATE1 and DATE2. +Both dates are (year month day ...) lists. +Returns float number of days (positive if date2 > date1)." + (let ((t1 (calendar-sync--date-to-time (list (nth 0 date1) (nth 1 date1) (nth 2 date1)))) + (t2 (calendar-sync--date-to-time (list (nth 0 date2) (nth 1 date2) (nth 2 date2))))) + (/ (float-time (time-subtract t2 t1)) 86400.0))) + +(defun test-calendar-sync-wide-range () + "Generate wide date range: 90 days past to 365 days future. +Returns (start-time end-time) suitable for expansion functions." + (list (time-subtract (current-time) (* 90 86400)) + (time-add (current-time) (* 365 86400)))) + +(defun test-calendar-sync-narrow-range () + "Generate narrow date range: today to 30 days future. +Returns (start-time end-time) suitable for expansion functions." + (list (current-time) + (time-add (current-time) (* 30 86400)))) + +(defun test-calendar-sync-date-to-time-value (date) + "Convert DATE list to Emacs time value. +DATE is (year month day) or (year month day hour minute)." + (let ((year (nth 0 date)) + (month (nth 1 date)) + (day (nth 2 date)) + (hour (or (nth 3 date) 0)) + (minute (or (nth 4 date) 0))) + (encode-time 0 minute hour day month year))) + (provide 'testutil-calendar-sync) ;;; testutil-calendar-sync.el ends here |
