diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-01 08:13:33 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-01 08:13:33 -0500 |
| commit | fdc63ea6675709d6ca42362b1be1918f8bf24478 (patch) | |
| tree | 056dd89c3879b4fd4e928ce8c6772501a7d7ffa5 | |
| parent | 0d55edecf82b32b75780f24751dfa8d693b72708 (diff) | |
Add 47 new unit tests across 7 test files covering playlist-modified-p,
assert-valid-playlist-file, playlist-tracks, create-radio-station,
ensure-playlist-buffer, after-playlist-clear, and header-text.
Extract three helpers to reduce duplication: assert-m3u-files-exist (dedupes
2 identical guards), sync-playlist-file (dedupes 3 state-sync patterns),
and select-m3u-file (reusable M3U selection with cancel). Simplify
append-track-to-playlist nesting from 6 to 4 levels. Delete unused
cj/music-keymap-prefix variable.
| -rw-r--r-- | modules/music-config.el | 81 | ||||
| -rw-r--r-- | tests/test-music-config--after-playlist-clear.el | 116 | ||||
| -rw-r--r-- | tests/test-music-config--assert-valid-playlist-file.el | 131 | ||||
| -rw-r--r-- | tests/test-music-config--ensure-playlist-buffer.el | 106 | ||||
| -rw-r--r-- | tests/test-music-config--header-text.el | 156 | ||||
| -rw-r--r-- | tests/test-music-config--playlist-modified-p.el | 145 | ||||
| -rw-r--r-- | tests/test-music-config--playlist-tracks.el | 123 | ||||
| -rw-r--r-- | tests/test-music-config-create-radio-station.el | 153 |
8 files changed, 975 insertions, 36 deletions
diff --git a/modules/music-config.el b/modules/music-config.el index d24018ac..46474a23 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -92,9 +92,6 @@ (defvar cj/music-m3u-root cj/music-root "Directory where M3U playlists are saved and loaded.") -(defvar cj/music-keymap-prefix (kbd "C-; m") - "Prefix keybinding for all music commands. Currently not auto-bound.") - (defvar cj/music-file-extensions '("aac" "flac" "m4a" "mp3" "ogg" "opus" "wav") "List of valid music file extensions.") @@ -223,6 +220,29 @@ Signals user-error if missing or deleted." (user-error "Playlist file no longer exists: %s" (file-name-nondirectory cj/music-playlist-file)))))) +(defun cj/music--assert-m3u-files-exist () + "Assert that M3U files exist in cj/music-m3u-root. +Returns the list of (BASENAME . FULLPATH) conses. Signals user-error if none." + (let ((files (cj/music--get-m3u-files))) + (when (null files) + (user-error "No M3U files found in %s" cj/music-m3u-root)) + files)) + +(defun cj/music--sync-playlist-file (file-path) + "Set the playlist buffer's associated M3U file to FILE-PATH and reset point." + (with-current-buffer (cj/music--ensure-playlist-buffer) + (setq cj/music-playlist-file file-path) + (goto-char (point-min)))) + +(defun cj/music--select-m3u-file (prompt) + "Prompt user to select an M3U file with PROMPT. +Returns the full path to the selected file, or nil if cancelled." + (let* ((m3u-files (cj/music--assert-m3u-files-exist)) + (choices (append (mapcar #'car m3u-files) '("(Cancel)"))) + (choice (completing-read prompt choices nil t))) + (unless (string= choice "(Cancel)") + (cdr (assoc choice m3u-files))))) + ;;; Commands: add/select (defun cj/music-add-directory-recursive (directory) @@ -300,40 +320,31 @@ Prompts for M3U file selection with completion. Allows cancellation." (unless track (user-error "No track at point")) (let* ((track-path (emms-track-name track)) - (m3u-files (cj/music--get-m3u-files))) - (when (null m3u-files) - (user-error "No M3U files found in %s" cj/music-m3u-root)) - (let* ((choices (append (mapcar #'car m3u-files) '("(Cancel)"))) - (choice (completing-read "Append track to playlist: " choices nil t))) - (if (string= choice "(Cancel)") - (message "Cancelled") - (let ((m3u-file (cdr (assoc choice m3u-files)))) - (condition-case err - (progn - (cj/music--append-track-to-m3u-file track-path m3u-file) - (message "Added '%s' to %s" - (file-name-nondirectory track-path) - choice)) - (error (message "Failed to append track: %s" (error-message-string err)))))))))) + (m3u-file (cj/music--select-m3u-file "Append track to playlist: "))) + (if (not m3u-file) + (message "Cancelled") + (condition-case err + (progn + (cj/music--append-track-to-m3u-file track-path m3u-file) + (message "Added '%s' to %s" + (file-name-nondirectory track-path) + (file-name-nondirectory m3u-file))) + (error (message "Failed to append track: %s" (error-message-string err)))))))) (defun cj/music-playlist-load () "Load an M3U playlist from cj/music-m3u-root. Replaces current playlist." (interactive) - (let* ((pairs (cj/music--get-m3u-files))) - (when (null pairs) - (user-error "No M3U files found in %s" cj/music-m3u-root)) - (let* ((choice-name (completing-read "Select playlist: " (mapcar #'car pairs) nil t)) - (choice-file (cdr (assoc choice-name pairs)))) - (unless (and choice-file (file-exists-p choice-file)) - (user-error "Playlist file does not exist: %s" choice-name)) - (emms-playlist-clear) - (emms-play-playlist choice-file) - (with-current-buffer (cj/music--ensure-playlist-buffer) - (setq cj/music-playlist-file choice-file) - (goto-char (point-min))) - (message "Loaded playlist: %s" choice-name)))) + (let* ((pairs (cj/music--assert-m3u-files-exist)) + (choice-name (completing-read "Select playlist: " (mapcar #'car pairs) nil t)) + (choice-file (cdr (assoc choice-name pairs)))) + (unless (and choice-file (file-exists-p choice-file)) + (user-error "Playlist file does not exist: %s" choice-name)) + (emms-playlist-clear) + (emms-play-playlist choice-file) + (cj/music--sync-playlist-file choice-file) + (message "Loaded playlist: %s" choice-name))) (defun cj/music-playlist-save () @@ -352,8 +363,8 @@ Offers completion over existing names but allows new names." (user-error "Aborted saving playlist")) (with-current-buffer (cj/music--ensure-playlist-buffer) (let ((emms-source-playlist-ask-before-overwrite nil)) - (emms-playlist-save 'm3u full)) - (setq cj/music-playlist-file full)) + (emms-playlist-save 'm3u full))) + (cj/music--sync-playlist-file full) (message "Saved playlist: %s" filename))) @@ -377,9 +388,7 @@ Offers completion over existing names but allows new names." (name (file-name-nondirectory file-path))) (emms-playlist-clear) (emms-play-playlist file-path) - (with-current-buffer (cj/music--ensure-playlist-buffer) - (setq cj/music-playlist-file file-path) - (goto-char (point-min))) + (cj/music--sync-playlist-file file-path) (message "Reloaded playlist: %s" name))) diff --git a/tests/test-music-config--after-playlist-clear.el b/tests/test-music-config--after-playlist-clear.el new file mode 100644 index 00000000..c23e2b5b --- /dev/null +++ b/tests/test-music-config--after-playlist-clear.el @@ -0,0 +1,116 @@ +;;; test-music-config--after-playlist-clear.el --- Tests for playlist clear advice -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--after-playlist-clear function. +;; Tests the advice function that resets cj/music-playlist-file +;; when the EMMS playlist is cleared. +;; +;; Test organization: +;; - Normal Cases: Clears file variable, noop when no buffer +;; - Boundary Cases: Already nil stays nil, idempotent on multiple calls +;; - Error Cases: Killed buffer doesn't crash +;; +;;; Code: + +(require 'ert) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-after-clear--setup () + "Create playlist buffer with a playlist file set." + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t)) + (setq emms-playlist-buffer buf) + buf)) + +(defun test-after-clear--teardown () + "Clean up test playlist buffer." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (setq cj/music-playlist-file nil)) + (kill-buffer buf))) + +;;; Normal Cases + +(ert-deftest test-music-config--after-playlist-clear-normal-clears-file-variable () + "Calling after-playlist-clear sets cj/music-playlist-file to nil." + (unwind-protect + (progn + (test-after-clear--setup) + (with-current-buffer cj/music-playlist-buffer-name + (setq cj/music-playlist-file "/path/to/playlist.m3u")) + (cj/music--after-playlist-clear) + (with-current-buffer cj/music-playlist-buffer-name + (should-not cj/music-playlist-file))) + (test-after-clear--teardown))) + +(ert-deftest test-music-config--after-playlist-clear-normal-noop-when-no-buffer () + "Does nothing when playlist buffer doesn't exist." + (unwind-protect + (progn + ;; Ensure no buffer exists + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf)) + ;; Should not error + (cj/music--after-playlist-clear)) + (test-after-clear--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--after-playlist-clear-boundary-already-nil-stays-nil () + "Already-nil playlist file remains nil after clear." + (unwind-protect + (progn + (test-after-clear--setup) + (with-current-buffer cj/music-playlist-buffer-name + (setq cj/music-playlist-file nil)) + (cj/music--after-playlist-clear) + (with-current-buffer cj/music-playlist-buffer-name + (should-not cj/music-playlist-file))) + (test-after-clear--teardown))) + +(ert-deftest test-music-config--after-playlist-clear-boundary-multiple-calls-idempotent () + "Multiple calls produce same result as single call." + (unwind-protect + (progn + (test-after-clear--setup) + (with-current-buffer cj/music-playlist-buffer-name + (setq cj/music-playlist-file "/path/to/playlist.m3u")) + (cj/music--after-playlist-clear) + (cj/music--after-playlist-clear) + (cj/music--after-playlist-clear) + (with-current-buffer cj/music-playlist-buffer-name + (should-not cj/music-playlist-file))) + (test-after-clear--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--after-playlist-clear-error-killed-buffer-no-crash () + "Killed buffer doesn't cause an error." + (let ((buf (test-after-clear--setup))) + (kill-buffer buf) + ;; Should not error when buffer is gone + (should-not (condition-case err + (progn (cj/music--after-playlist-clear) nil) + (error err))))) + +(provide 'test-music-config--after-playlist-clear) +;;; test-music-config--after-playlist-clear.el ends here diff --git a/tests/test-music-config--assert-valid-playlist-file.el b/tests/test-music-config--assert-valid-playlist-file.el new file mode 100644 index 00000000..aca0eeb9 --- /dev/null +++ b/tests/test-music-config--assert-valid-playlist-file.el @@ -0,0 +1,131 @@ +;;; test-music-config--assert-valid-playlist-file.el --- Tests for playlist file validation -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--assert-valid-playlist-file function. +;; Tests the validation guard that ensures a playlist buffer has +;; a valid, existing associated M3U file before operations proceed. +;; +;; Test organization: +;; - Normal Cases: Valid file passes without error +;; - Boundary Cases: Empty file, file exists but is empty +;; - Error Cases: Nil file, nonexistent file, deleted file +;; +;;; Code: + +(require 'ert) +(require 'testutil-general) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-assert--setup () + "Create test base dir and ensure playlist buffer exists." + (cj/create-test-base-dir) + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t)) + (setq emms-playlist-buffer buf) + buf)) + +(defun test-assert--teardown () + "Clean up test playlist buffer and temp files." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (setq cj/music-playlist-file nil)) + (kill-buffer buf)) + (cj/delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-music-config--assert-valid-playlist-file-normal-existing-file-passes () + "Valid existing file does not signal error." + (test-assert--setup) + (unwind-protect + (let ((file (cj/create-temp-test-file "playlist-"))) + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file file)) + ;; Should not error + (cj/music--assert-valid-playlist-file)) + (test-assert--teardown))) + +(ert-deftest test-music-config--assert-valid-playlist-file-normal-file-with-content-passes () + "Valid file with content does not signal error." + (test-assert--setup) + (unwind-protect + (let ((file (cj/create-temp-test-file-with-content "track1.mp3\ntrack2.mp3\n" "playlist-"))) + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file file)) + (cj/music--assert-valid-playlist-file)) + (test-assert--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--assert-valid-playlist-file-boundary-empty-file-passes () + "Empty but existing file does not signal error." + (test-assert--setup) + (unwind-protect + (let ((file (cj/create-temp-test-file "playlist-"))) + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file file)) + ;; File exists but is empty - should still pass + (cj/music--assert-valid-playlist-file)) + (test-assert--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--assert-valid-playlist-file-error-nil-file-signals-user-error () + "Nil cj/music-playlist-file signals user-error." + (test-assert--setup) + (unwind-protect + (progn + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file nil)) + (should-error (cj/music--assert-valid-playlist-file) + :type 'user-error)) + (test-assert--teardown))) + +(ert-deftest test-music-config--assert-valid-playlist-file-error-nonexistent-file-signals-user-error () + "Nonexistent file path signals user-error." + (test-assert--setup) + (unwind-protect + (progn + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file "/nonexistent/path/to/playlist.m3u")) + (should-error (cj/music--assert-valid-playlist-file) + :type 'user-error)) + (test-assert--teardown))) + +(ert-deftest test-music-config--assert-valid-playlist-file-error-deleted-file-signals-user-error () + "File that existed but was deleted signals user-error." + (test-assert--setup) + (unwind-protect + (let ((file (cj/create-temp-test-file "playlist-"))) + (with-current-buffer (get-buffer cj/music-playlist-buffer-name) + (setq cj/music-playlist-file file)) + ;; Verify it passes first + (cj/music--assert-valid-playlist-file) + ;; Now delete the file + (delete-file file) + ;; Should now signal error + (should-error (cj/music--assert-valid-playlist-file) + :type 'user-error)) + (test-assert--teardown))) + +(provide 'test-music-config--assert-valid-playlist-file) +;;; test-music-config--assert-valid-playlist-file.el ends here diff --git a/tests/test-music-config--ensure-playlist-buffer.el b/tests/test-music-config--ensure-playlist-buffer.el new file mode 100644 index 00000000..b7f9a8b7 --- /dev/null +++ b/tests/test-music-config--ensure-playlist-buffer.el @@ -0,0 +1,106 @@ +;;; test-music-config--ensure-playlist-buffer.el --- Tests for playlist buffer creation -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--ensure-playlist-buffer function. +;; Tests buffer creation, mode setup, and global variable assignment. +;; +;; Test organization: +;; - Normal Cases: Creates new buffer, returns existing buffer +;; - Boundary Cases: Wrong mode gets corrected, sets global variable +;; - Error Cases: Killed buffer gets recreated, returns live buffer +;; +;;; Code: + +(require 'ert) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-ensure--teardown () + "Clean up test playlist buffer." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf))) + +;;; Normal Cases + +(ert-deftest test-music-config--ensure-playlist-buffer-normal-creates-buffer-when-none-exists () + "Creates a new playlist buffer when none exists." + (unwind-protect + (progn + ;; Kill any existing buffer + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf)) + (let ((result (cj/music--ensure-playlist-buffer))) + (should (bufferp result)) + (should (buffer-live-p result)) + (should (equal (buffer-name result) cj/music-playlist-buffer-name)))) + (test-ensure--teardown))) + +(ert-deftest test-music-config--ensure-playlist-buffer-normal-returns-existing-buffer () + "Returns existing buffer without recreating it." + (unwind-protect + (let* ((buf1 (cj/music--ensure-playlist-buffer)) + (buf2 (cj/music--ensure-playlist-buffer))) + (should (eq buf1 buf2))) + (test-ensure--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--ensure-playlist-buffer-boundary-wrong-mode-corrected () + "Buffer in wrong mode gets corrected to emms-playlist-mode." + (unwind-protect + (progn + ;; Create buffer in fundamental-mode + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (fundamental-mode))) + (let ((result (cj/music--ensure-playlist-buffer))) + (with-current-buffer result + (should (eq major-mode 'emms-playlist-mode))))) + (test-ensure--teardown))) + +(ert-deftest test-music-config--ensure-playlist-buffer-boundary-sets-global-variable () + "Sets emms-playlist-buffer to the returned buffer." + (unwind-protect + (let ((result (cj/music--ensure-playlist-buffer))) + (should (eq emms-playlist-buffer result))) + (test-ensure--teardown))) + +(ert-deftest test-music-config--ensure-playlist-buffer-boundary-sets-buffer-local-flag () + "Sets emms-playlist-buffer-p to t in the buffer." + (unwind-protect + (let ((result (cj/music--ensure-playlist-buffer))) + (with-current-buffer result + (should (eq emms-playlist-buffer-p t)))) + (test-ensure--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--ensure-playlist-buffer-error-killed-buffer-recreated () + "After buffer is killed, calling again creates a new live buffer." + (unwind-protect + (progn + (let ((buf1 (cj/music--ensure-playlist-buffer))) + (kill-buffer buf1)) + (let ((buf2 (cj/music--ensure-playlist-buffer))) + (should (buffer-live-p buf2)) + (should (equal (buffer-name buf2) cj/music-playlist-buffer-name)))) + (test-ensure--teardown))) + +(provide 'test-music-config--ensure-playlist-buffer) +;;; test-music-config--ensure-playlist-buffer.el ends here diff --git a/tests/test-music-config--header-text.el b/tests/test-music-config--header-text.el new file mode 100644 index 00000000..8de97350 --- /dev/null +++ b/tests/test-music-config--header-text.el @@ -0,0 +1,156 @@ +;;; test-music-config--header-text.el --- Tests for playlist header string generation -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--header-text function. +;; Tests the multi-line header string builder for the playlist overlay. +;; +;; Test organization: +;; - Normal Cases: Playlist name from file, "Untitled" default, track count +;; - Boundary Cases: Stopped state, paused state, mode indicators +;; - Error Cases: Empty playlist buffer +;; +;;; Code: + +(require 'ert) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-header--setup-playlist-buffer (track-names) + "Create an EMMS playlist buffer with TRACK-NAMES and return it." + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t) + (let ((inhibit-read-only t)) + (erase-buffer) + (dolist (name track-names) + (emms-playlist-insert-track (emms-track 'file name))))) + (setq emms-playlist-buffer buf) + buf)) + +(defun test-header--teardown () + "Clean up test playlist buffer and reset EMMS state." + (setq emms-player-playing-p nil) + (setq emms-player-paused-p nil) + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (setq cj/music-playlist-file nil)) + (kill-buffer buf))) + +(defun test-header--strip-properties (str) + "Return STR with all text properties removed." + (let ((s (copy-sequence str))) + (set-text-properties 0 (length s) nil s) + s)) + +;;; Normal Cases + +(ert-deftest test-music-config--header-text-normal-shows-playlist-name-from-file () + "Header shows playlist name derived from cj/music-playlist-file." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3")) + (with-current-buffer cj/music-playlist-buffer-name + (setq cj/music-playlist-file "/path/to/my-jazz.m3u")) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "my-jazz" plain)))) + (test-header--teardown))) + +(ert-deftest test-music-config--header-text-normal-shows-untitled-when-no-file () + "Header shows 'Untitled' when no playlist file is associated." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3")) + (with-current-buffer cj/music-playlist-buffer-name + (setq cj/music-playlist-file nil)) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "Untitled" plain)))) + (test-header--teardown))) + +(ert-deftest test-music-config--header-text-normal-shows-track-count () + "Header shows correct track count." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3" "/music/b.mp3" "/music/c.mp3")) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "(3)" plain)))) + (test-header--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--header-text-boundary-stopped-state () + "Header shows 'Stopped' when not playing." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3")) + (setq emms-player-playing-p nil) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "Stopped" plain)))) + (test-header--teardown))) + +(ert-deftest test-music-config--header-text-boundary-paused-state () + "Header shows 'Paused' when player is paused." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3")) + (setq emms-player-playing-p t) + (setq emms-player-paused-p t) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "Paused" plain)))) + (test-header--teardown))) + +(ert-deftest test-music-config--header-text-boundary-contains-mode-labels () + "Header contains mode indicator labels for repeat, single, random, consume." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '("/music/a.mp3")) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "repeat" plain)) + (should (string-match-p "single" plain)) + (should (string-match-p "random" plain)) + (should (string-match-p "consume" plain)))) + (test-header--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--header-text-error-empty-playlist-shows-zero-count () + "Header shows (0) for empty playlist." + (unwind-protect + (progn + (test-header--setup-playlist-buffer '()) + (let* ((header (with-current-buffer cj/music-playlist-buffer-name + (cj/music--header-text))) + (plain (test-header--strip-properties header))) + (should (string-match-p "(0)" plain)))) + (test-header--teardown))) + +(provide 'test-music-config--header-text) +;;; test-music-config--header-text.el ends here diff --git a/tests/test-music-config--playlist-modified-p.el b/tests/test-music-config--playlist-modified-p.el new file mode 100644 index 00000000..cbfc71e3 --- /dev/null +++ b/tests/test-music-config--playlist-modified-p.el @@ -0,0 +1,145 @@ +;;; test-music-config--playlist-modified-p.el --- Tests for playlist modification detection -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--playlist-modified-p function. +;; Tests the logic that compares the current EMMS playlist buffer +;; against its associated M3U file to detect unsaved changes. +;; +;; Test organization: +;; - Normal Cases: Matching/differing track lists +;; - Boundary Cases: Empty playlists, nil file, ordering +;; - Error Cases: Missing M3U file, missing buffer +;; +;;; Code: + +(require 'ert) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-modified--setup-playlist-buffer (track-names) + "Create an EMMS playlist buffer with TRACK-NAMES and return it. +Each entry in TRACK-NAMES becomes a file track in the playlist." + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t) + (let ((inhibit-read-only t)) + (erase-buffer) + (dolist (name track-names) + (emms-playlist-insert-track (emms-track 'file name))))) + (setq emms-playlist-buffer buf) + buf)) + +(defun test-modified--teardown () + "Clean up test playlist buffer." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf))) + +;;; Normal Cases + +(ert-deftest test-music-config--playlist-modified-p-normal-identical-tracks-returns-nil () + "Identical track lists in file and playlist returns nil (not modified)." + (unwind-protect + (let* ((tracks '("/music/a.mp3" "/music/b.mp3" "/music/c.mp3")) + (cj/music-playlist-file "/fake/playlist.m3u")) + (test-modified--setup-playlist-buffer tracks) + (cl-letf (((symbol-function 'cj/music--m3u-file-tracks) + (lambda (_file) tracks))) + (should-not (cj/music--playlist-modified-p)))) + (test-modified--teardown))) + +(ert-deftest test-music-config--playlist-modified-p-normal-different-tracks-returns-non-nil () + "Different track lists returns non-nil (modified)." + (unwind-protect + (let* ((file-tracks '("/music/a.mp3" "/music/b.mp3")) + (buf-tracks '("/music/a.mp3" "/music/c.mp3")) + (cj/music-playlist-file "/fake/playlist.m3u")) + (test-modified--setup-playlist-buffer buf-tracks) + (cl-letf (((symbol-function 'cj/music--m3u-file-tracks) + (lambda (_file) file-tracks))) + (should (cj/music--playlist-modified-p)))) + (test-modified--teardown))) + +(ert-deftest test-music-config--playlist-modified-p-normal-extra-track-returns-non-nil () + "Playlist with extra track compared to file returns non-nil." + (unwind-protect + (let* ((file-tracks '("/music/a.mp3" "/music/b.mp3")) + (buf-tracks '("/music/a.mp3" "/music/b.mp3" "/music/c.mp3")) + (cj/music-playlist-file "/fake/playlist.m3u")) + (test-modified--setup-playlist-buffer buf-tracks) + (cl-letf (((symbol-function 'cj/music--m3u-file-tracks) + (lambda (_file) file-tracks))) + (should (cj/music--playlist-modified-p)))) + (test-modified--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--playlist-modified-p-boundary-no-playlist-file-returns-nil () + "Nil cj/music-playlist-file returns nil without comparing tracks." + (unwind-protect + (let ((cj/music-playlist-file nil)) + (test-modified--setup-playlist-buffer '("/music/a.mp3")) + (should-not (cj/music--playlist-modified-p))) + (test-modified--teardown))) + +(ert-deftest test-music-config--playlist-modified-p-boundary-empty-file-and-playlist-returns-nil () + "Both empty file tracks and empty playlist returns nil." + (unwind-protect + (let ((cj/music-playlist-file "/fake/playlist.m3u")) + (test-modified--setup-playlist-buffer '()) + (cl-letf (((symbol-function 'cj/music--m3u-file-tracks) + (lambda (_file) nil))) + (should-not (cj/music--playlist-modified-p)))) + (test-modified--teardown))) + +(ert-deftest test-music-config--playlist-modified-p-boundary-order-difference-returns-non-nil () + "Same tracks in different order returns non-nil." + (unwind-protect + (let* ((file-tracks '("/music/a.mp3" "/music/b.mp3")) + (buf-tracks '("/music/b.mp3" "/music/a.mp3")) + (cj/music-playlist-file "/fake/playlist.m3u")) + (test-modified--setup-playlist-buffer buf-tracks) + (cl-letf (((symbol-function 'cj/music--m3u-file-tracks) + (lambda (_file) file-tracks))) + (should (cj/music--playlist-modified-p)))) + (test-modified--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--playlist-modified-p-error-missing-m3u-file-returns-nil () + "When M3U file doesn't exist, m3u-file-tracks returns nil; empty playlist matches." + (unwind-protect + (let ((cj/music-playlist-file "/nonexistent/playlist.m3u")) + (test-modified--setup-playlist-buffer '()) + ;; cj/music--m3u-file-tracks returns nil for nonexistent files + ;; empty playlist also returns nil, so equal = not modified + (should-not (cj/music--playlist-modified-p))) + (test-modified--teardown))) + +(ert-deftest test-music-config--playlist-modified-p-error-missing-m3u-with-tracks-returns-non-nil () + "When M3U file doesn't exist but playlist has tracks, returns non-nil." + (unwind-protect + (let ((cj/music-playlist-file "/nonexistent/playlist.m3u")) + (test-modified--setup-playlist-buffer '("/music/a.mp3")) + ;; m3u-file-tracks returns nil, but playlist has tracks = modified + (should (cj/music--playlist-modified-p))) + (test-modified--teardown))) + +(provide 'test-music-config--playlist-modified-p) +;;; test-music-config--playlist-modified-p.el ends here diff --git a/tests/test-music-config--playlist-tracks.el b/tests/test-music-config--playlist-tracks.el new file mode 100644 index 00000000..7736b8d2 --- /dev/null +++ b/tests/test-music-config--playlist-tracks.el @@ -0,0 +1,123 @@ +;;; test-music-config--playlist-tracks.el --- Tests for playlist track extraction -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music--playlist-tracks function. +;; Tests reading track names from the current EMMS playlist buffer. +;; +;; Test organization: +;; - Normal Cases: Single and multiple tracks +;; - Boundary Cases: Empty playlist, order preservation +;; - Error Cases: No playlist buffer, tracks without names +;; +;;; Code: + +(require 'ert) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Add EMMS elpa directory to load path for batch testing +(let ((emms-dir (car (file-expand-wildcards + (expand-file-name "elpa/emms-*" user-emacs-directory))))) + (when emms-dir + (add-to-list 'load-path emms-dir))) + +(require 'emms) +(require 'emms-playlist-mode) +(require 'music-config) + +;;; Test helpers + +(defun test-tracks--setup-playlist-buffer (track-names) + "Create an EMMS playlist buffer with TRACK-NAMES and return it. +Each entry in TRACK-NAMES becomes a file track in the playlist." + (let ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t) + (let ((inhibit-read-only t)) + (erase-buffer) + (dolist (name track-names) + (emms-playlist-insert-track (emms-track 'file name))))) + (setq emms-playlist-buffer buf) + buf)) + +(defun test-tracks--teardown () + "Clean up test playlist buffer." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf))) + +;;; Normal Cases + +(ert-deftest test-music-config--playlist-tracks-normal-multiple-tracks-returns-list () + "Multiple tracks in playlist returns list of track names." + (unwind-protect + (let ((tracks '("/music/a.mp3" "/music/b.mp3" "/music/c.mp3"))) + (test-tracks--setup-playlist-buffer tracks) + (should (equal (cj/music--playlist-tracks) tracks))) + (test-tracks--teardown))) + +(ert-deftest test-music-config--playlist-tracks-normal-single-track-returns-list () + "Single track in playlist returns single-element list." + (unwind-protect + (let ((tracks '("/music/only.mp3"))) + (test-tracks--setup-playlist-buffer tracks) + (should (equal (cj/music--playlist-tracks) tracks))) + (test-tracks--teardown))) + +;;; Boundary Cases + +(ert-deftest test-music-config--playlist-tracks-boundary-empty-playlist-returns-nil () + "Empty playlist returns nil (empty list)." + (unwind-protect + (progn + (test-tracks--setup-playlist-buffer '()) + (should-not (cj/music--playlist-tracks))) + (test-tracks--teardown))) + +(ert-deftest test-music-config--playlist-tracks-boundary-preserves-insertion-order () + "Track order matches insertion order." + (unwind-protect + (let ((tracks '("/music/z.mp3" "/music/a.mp3" "/music/m.mp3"))) + (test-tracks--setup-playlist-buffer tracks) + (should (equal (cj/music--playlist-tracks) tracks))) + (test-tracks--teardown))) + +(ert-deftest test-music-config--playlist-tracks-boundary-url-tracks-included () + "URL tracks are included alongside file tracks." + (unwind-protect + (let* ((buf (get-buffer-create cj/music-playlist-buffer-name))) + (with-current-buffer buf + (emms-playlist-mode) + (setq emms-playlist-buffer-p t) + (let ((inhibit-read-only t)) + (erase-buffer) + (emms-playlist-insert-track (emms-track 'file "/music/local.mp3")) + (emms-playlist-insert-track (emms-track 'url "http://stream.example.com/radio")))) + (setq emms-playlist-buffer buf) + (let ((result (cj/music--playlist-tracks))) + (should (= 2 (length result))) + (should (equal (nth 0 result) "/music/local.mp3")) + (should (equal (nth 1 result) "http://stream.example.com/radio")))) + (test-tracks--teardown))) + +;;; Error Cases + +(ert-deftest test-music-config--playlist-tracks-error-no-buffer-creates-one () + "When playlist buffer doesn't exist, ensure-playlist-buffer creates it." + (unwind-protect + (progn + ;; Kill any existing buffer + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (kill-buffer buf)) + ;; Should create buffer and return empty list + (should-not (cj/music--playlist-tracks)) + ;; Buffer should now exist + (should (get-buffer cj/music-playlist-buffer-name))) + (test-tracks--teardown))) + +(provide 'test-music-config--playlist-tracks) +;;; test-music-config--playlist-tracks.el ends here diff --git a/tests/test-music-config-create-radio-station.el b/tests/test-music-config-create-radio-station.el new file mode 100644 index 00000000..1f4365a4 --- /dev/null +++ b/tests/test-music-config-create-radio-station.el @@ -0,0 +1,153 @@ +;;; test-music-config-create-radio-station.el --- Tests for radio station creation -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Unit tests for cj/music-create-radio-station function. +;; Tests M3U file creation for radio stations with stream URLs. +;; +;; Test organization: +;; - Normal Cases: Standard creation, EXTM3U format, safe filename +;; - Boundary Cases: Unicode name, complex URL, overwrite confirmed +;; - Error Cases: Empty name, empty URL, overwrite declined +;; +;;; Code: + +(require 'ert) +(require 'testutil-general) + +;; Stub missing dependencies before loading music-config +(defvar-keymap cj/custom-keymap + :doc "Stub keymap for testing") + +;; Load production code +(require 'music-config) + +;;; Setup & Teardown + +(defun test-music-config-create-radio-station-setup () + "Setup test environment with temp directory for M3U output." + (cj/create-test-base-dir) + (cj/create-test-subdirectory "radio-playlists")) + +(defun test-music-config-create-radio-station-teardown () + "Clean up test environment." + (cj/delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-music-config-create-radio-station-normal-creates-m3u-file () + "Creating a radio station produces an M3U file in the music root." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + (cj/music-create-radio-station "Jazz FM" "http://stream.jazzfm.com/radio") + (let ((expected-file (expand-file-name "Jazz_FM_Radio.m3u" test-dir))) + (should (file-exists-p expected-file)))) + (test-music-config-create-radio-station-teardown)))) + +(ert-deftest test-music-config-create-radio-station-normal-extm3u-format () + "Created file contains EXTM3U header, EXTINF with station name, and URL." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + (cj/music-create-radio-station "Jazz FM" "http://stream.jazzfm.com/radio") + (let ((content (with-temp-buffer + (insert-file-contents + (expand-file-name "Jazz_FM_Radio.m3u" test-dir)) + (buffer-string)))) + (should (string-match-p "^#EXTM3U" content)) + (should (string-match-p "#EXTINF:-1,Jazz FM" content)) + (should (string-match-p "http://stream.jazzfm.com/radio" content)))) + (test-music-config-create-radio-station-teardown)))) + +(ert-deftest test-music-config-create-radio-station-normal-safe-filename () + "Station name with special characters produces filesystem-safe filename." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + (cj/music-create-radio-station "Rock & Roll 101.5" "http://example.com/stream") + ;; Spaces and special chars replaced with underscores + (let ((expected-file (expand-file-name "Rock___Roll_101_5_Radio.m3u" test-dir))) + (should (file-exists-p expected-file)))) + (test-music-config-create-radio-station-teardown)))) + +;;; Boundary Cases + +(ert-deftest test-music-config-create-radio-station-boundary-unicode-name-safe-filename () + "Unicode station name produces safe filename while preserving name in EXTINF." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + (cj/music-create-radio-station "Klassik Radio" "http://example.com/stream") + ;; Name is all ASCII-safe, so filename uses it directly + (should (file-exists-p (expand-file-name "Klassik_Radio_Radio.m3u" test-dir))) + ;; Original name preserved in EXTINF inside the file + (let ((content (with-temp-buffer + (insert-file-contents + (expand-file-name "Klassik_Radio_Radio.m3u" test-dir)) + (buffer-string)))) + (should (string-match-p "Klassik Radio" content)))) + (test-music-config-create-radio-station-teardown)))) + +(ert-deftest test-music-config-create-radio-station-boundary-url-with-query-params () + "Complex URL with query parameters preserved in file content." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir) + (url "https://stream.example.com/radio?format=mp3&quality=320&token=abc123")) + (cj/music-create-radio-station "Test Radio" url) + (let ((content (with-temp-buffer + (insert-file-contents + (expand-file-name "Test_Radio_Radio.m3u" test-dir)) + (buffer-string)))) + (should (string-match-p (regexp-quote url) content)))) + (test-music-config-create-radio-station-teardown)))) + +(ert-deftest test-music-config-create-radio-station-boundary-overwrite-confirmed () + "Overwriting existing file when user confirms succeeds." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + ;; Create initial file + (cj/music-create-radio-station "MyRadio" "http://old.url/stream") + (let ((file (expand-file-name "MyRadio_Radio.m3u" test-dir))) + (should (file-exists-p file)) + ;; Overwrite with user confirming + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_prompt) t))) + (cj/music-create-radio-station "MyRadio" "http://new.url/stream")) + ;; File should now contain new URL + (let ((content (with-temp-buffer + (insert-file-contents file) + (buffer-string)))) + (should (string-match-p "http://new.url/stream" content)) + (should-not (string-match-p "http://old.url/stream" content))))) + (test-music-config-create-radio-station-teardown)))) + +;;; Error Cases + +(ert-deftest test-music-config-create-radio-station-error-empty-name-signals-user-error () + "Empty station name signals user-error." + (should-error (cj/music-create-radio-station "" "http://example.com/stream") + :type 'user-error)) + +(ert-deftest test-music-config-create-radio-station-error-empty-url-signals-user-error () + "Empty URL signals user-error." + (should-error (cj/music-create-radio-station "Test Radio" "") + :type 'user-error)) + +(ert-deftest test-music-config-create-radio-station-error-overwrite-declined-signals-user-error () + "Declining overwrite signals user-error." + (let ((test-dir (test-music-config-create-radio-station-setup))) + (unwind-protect + (let ((cj/music-m3u-root test-dir)) + ;; Create initial file + (cj/music-create-radio-station "MyRadio" "http://old.url/stream") + ;; Decline overwrite + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_prompt) nil))) + (should-error (cj/music-create-radio-station "MyRadio" "http://new.url/stream") + :type 'user-error))) + (test-music-config-create-radio-station-teardown)))) + +(provide 'test-music-config-create-radio-station) +;;; test-music-config-create-radio-station.el ends here |
