summaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync-properties.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-calendar-sync-properties.el')
-rw-r--r--tests/test-calendar-sync-properties.el239
1 files changed, 239 insertions, 0 deletions
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