From 0afa3fba94157d5e18f9a086e0b67b7cfd2aedf0 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 17 Nov 2025 02:54:02 -0600 Subject: fix(calendar-sync): Remove carriage return characters from synced events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Google Calendar .ics files use CRLF line endings (RFC 5545 spec), which resulted in 11,685 ^M (CR) characters appearing in gcal.org, particularly at the end of org header lines. Solution: - Created calendar-sync--normalize-line-endings function to strip all \r characters from .ics content - Integrated into calendar-sync--fetch-ics immediately after curl download - Ensures clean Unix LF-only line endings throughout parsing pipeline Testing: - Added comprehensive test suite: test-calendar-sync--normalize-line-endings.el - 16 tests covering Normal, Boundary, and Error cases - All 56 existing calendar-sync tests still pass (no regressions) - Verified: gcal.org now has 0 CR characters (was 11,685) Files modified: - modules/calendar-sync.el: Added normalize function, updated fetch function - tests/test-calendar-sync--normalize-line-endings.el: New comprehensive test suite 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../test-calendar-sync--normalize-line-endings.el | 186 +++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 tests/test-calendar-sync--normalize-line-endings.el (limited to 'tests') diff --git a/tests/test-calendar-sync--normalize-line-endings.el b/tests/test-calendar-sync--normalize-line-endings.el new file mode 100644 index 00000000..7f0830cc --- /dev/null +++ b/tests/test-calendar-sync--normalize-line-endings.el @@ -0,0 +1,186 @@ +;;; test-calendar-sync--normalize-line-endings.el --- Tests for calendar-sync--normalize-line-endings -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for calendar-sync--normalize-line-endings function. +;; Tests conversion of various line ending formats to Unix LF-only format. +;; Covers Normal, Boundary, and Error cases. +;; +;; The iCalendar format (RFC 5545) uses CRLF line endings (\r\n), +;; but Emacs and org-mode expect LF only (\n). This function ensures +;; consistent line endings throughout the parsing pipeline. + +;;; Code: + +(require 'ert) +(require 'calendar-sync) + +;;; Normal Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-crlf-to-lf () + "Test that CRLF line endings are converted to LF only. + +Input: String with \\r\\n (Windows/DOS line endings) +Expected: String with \\n only (Unix line endings)" + (let* ((input "line1\r\nline2\r\nline3\r\n") + (expected "line1\nline2\nline3\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-lf-unchanged () + "Test that LF-only content is returned unchanged. + +Input: String with \\n only (already Unix format) +Expected: Same string (no modification needed)" + (let* ((input "line1\nline2\nline3\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= input result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-mixed-endings () + "Test that mixed line endings are normalized to LF only. + +Input: String with both \\r\\n (CRLF) and \\n (LF) +Expected: String with \\n only everywhere" + (let* ((input "line1\r\nline2\nline3\r\nline4\n") + (expected "line1\nline2\nline3\nline4\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-ics-vevent-block () + "Test normalization of realistic iCalendar VEVENT block with CRLF. + +Input: VEVENT block with CRLF line endings (as per RFC 5545) +Expected: Same structure with LF only" + (let* ((input "BEGIN:VEVENT\r\nSUMMARY:Test Event\r\nDTSTART:20251116T140000Z\r\nEND:VEVENT\r\n") + (expected "BEGIN:VEVENT\nSUMMARY:Test Event\nDTSTART:20251116T140000Z\nEND:VEVENT\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +;;; Boundary Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-empty-string () + "Test that empty string is handled correctly. + +Input: Empty string +Expected: Empty string (no crash)" + (let ((result (calendar-sync--normalize-line-endings ""))) + (should (string= "" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-no-line-endings () + "Test that string with no line endings is unchanged. + +Input: Plain text with no \\r or \\n +Expected: Same string unchanged" + (let* ((input "no line endings here") + (result (calendar-sync--normalize-line-endings input))) + (should (string= input result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-only-cr () + "Test that bare CR characters (old Mac format) are removed. + +Input: String with \\r only (classic Mac OS line endings) +Expected: String with \\r removed (results in run-together text)" + (let* ((input "line1\rline2\rline3\r") + (expected "line1line2line3") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-cr-in-middle () + "Test that CR characters in middle of content are removed. + +Input: String with \\r not followed by \\n (unusual but possible) +Expected: All \\r removed regardless of position" + (let* ((input "line1\r\ntext\rwith\rmiddle\r\nline2") + (expected "line1\ntextwithmiddle\nline2") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-multiple-cr () + "Test that multiple consecutive CR characters are all removed. + +Input: String with \\r\\r or \\r\\r\\n sequences +Expected: All \\r characters removed" + (let* ((input "line1\r\r\nline2\r\r\r\nline3") + (expected "line1\nline2\nline3") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-single-line () + "Test normalization of single line with trailing CRLF. + +Input: Single line of text ending with \\r\\n +Expected: Single line ending with \\n" + (let* ((input "single line\r\n") + (expected "single line\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-only-line-endings () + "Test string containing only line ending characters. + +Input: String of only \\r\\n sequences +Expected: String of only \\n (CR stripped)" + (let* ((input "\r\n\r\n\r\n") + (expected "\n\n\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-unicode-content () + "Test normalization preserves Unicode characters. + +Input: String with Unicode and CRLF line endings +Expected: Unicode preserved, only CR removed" + (let* ((input "emoji 🎉\r\nchinese 中文\r\narabic العربية\r\n") + (expected "emoji 🎉\nchinese 中文\narabic العربية\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-very-long-string () + "Test normalization of large string with many line endings. + +Input: String with 1000 lines with CRLF +Expected: Same content with LF only, performance acceptable" + (let* ((line "This is line content with some text\r\n") + (input (apply #'concat (make-list 1000 line))) + (result (calendar-sync--normalize-line-endings input))) + (should (= (length input) (+ (length result) 1000))) ; 1000 \r removed + (should-not (string-match-p "\r" result)) + (should (string-match-p "^This is line content" result)))) + +;;; Error Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-error-nil-input () + "Test that nil input is handled gracefully. + +Input: nil +Expected: nil (defensive programming, no crash)" + (let ((result (calendar-sync--normalize-line-endings nil))) + (should (null result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-error-non-string-input () + "Test that non-string input is returned unchanged. + +Input: Integer (wrong type) +Expected: Same value returned (defensive, don't crash)" + (let ((result (calendar-sync--normalize-line-endings 42))) + (should (= 42 result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-error-symbol-input () + "Test that symbol input is handled gracefully. + +Input: Symbol (wrong type) +Expected: Symbol returned unchanged" + (let ((result (calendar-sync--normalize-line-endings 'some-symbol))) + (should (eq 'some-symbol result)))) + +(provide 'test-calendar-sync--normalize-line-endings) +;;; test-calendar-sync--normalize-line-endings.el ends here -- cgit v1.2.3