summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/test-video-audio-recording--get-available-mics.el17
-rw-r--r--tests/test-video-audio-recording--get-available-sinks.el16
-rw-r--r--tests/test-video-audio-recording--get-sink-apps.el118
-rw-r--r--tests/test-video-audio-recording--label-devices.el110
-rw-r--r--tests/test-video-audio-recording--label-sinks.el111
5 files changed, 372 insertions, 0 deletions
diff --git a/tests/test-video-audio-recording--get-available-mics.el b/tests/test-video-audio-recording--get-available-mics.el
index 86a9d05c..5b549da3 100644
--- a/tests/test-video-audio-recording--get-available-mics.el
+++ b/tests/test-video-audio-recording--get-available-mics.el
@@ -109,5 +109,22 @@ Each source is (name description mute state)."
("sink-b.monitor" "Monitor B" "no" "SUSPENDED"))))))
(should (null (cj/recording--get-available-mics)))))
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording--get-available-mics-error-garbled-output ()
+ "Test that garbled pactl output returns empty list, not an error."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "random garbage\nwith newlines\nbut no structure\n")))
+ (should (null (cj/recording--get-available-mics)))))
+
+(ert-deftest test-video-audio-recording--get-available-mics-error-missing-fields ()
+ "Test that source with partial fields does not crash."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "Source #1\n\tState: RUNNING\n\tName: partial-source\n")))
+ ;; Missing Description and Mute — should not error
+ (let ((mics (cj/recording--get-available-mics)))
+ ;; May return empty or partial; must not signal
+ (should (listp mics)))))
+
(provide 'test-video-audio-recording--get-available-mics)
;;; test-video-audio-recording--get-available-mics.el ends here
diff --git a/tests/test-video-audio-recording--get-available-sinks.el b/tests/test-video-audio-recording--get-available-sinks.el
index 2f0d965c..62d9a41a 100644
--- a/tests/test-video-audio-recording--get-available-sinks.el
+++ b/tests/test-video-audio-recording--get-available-sinks.el
@@ -86,5 +86,21 @@ Each sink is (name description mute state)."
(lambda (_cmd) "")))
(should (null (cj/recording--get-available-sinks)))))
+;;; Error Cases
+
+(ert-deftest test-get-available-sinks-error-garbled-output ()
+ "Test that garbled pactl output returns empty list, not an error."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "random garbage\nwith newlines\nbut no structure\n")))
+ (should (null (cj/recording--get-available-sinks)))))
+
+(ert-deftest test-get-available-sinks-error-missing-fields ()
+ "Test that sink with partial fields does not crash."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "Sink #1\n\tState: RUNNING\n\tName: partial-sink\n")))
+ ;; Missing Description and Mute — should not error
+ (let ((sinks (cj/recording--get-available-sinks)))
+ (should (listp sinks)))))
+
(provide 'test-video-audio-recording--get-available-sinks)
;;; test-video-audio-recording--get-available-sinks.el ends here
diff --git a/tests/test-video-audio-recording--get-sink-apps.el b/tests/test-video-audio-recording--get-sink-apps.el
new file mode 100644
index 00000000..8712fa97
--- /dev/null
+++ b/tests/test-video-audio-recording--get-sink-apps.el
@@ -0,0 +1,118 @@
+;;; test-video-audio-recording--get-sink-apps.el --- Tests for sink app discovery -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording--get-sink-apps.
+;; Verifies parsing of `pactl list sink-inputs' output to map
+;; sink indices to application names.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+(require 'video-audio-recording)
+
+;;; Helpers
+
+(defvar test-sink-apps--dir
+ (file-name-directory (or load-file-name (locate-library "test-video-audio-recording--get-sink-apps")))
+ "Directory containing this test file.")
+
+(defun test-sink-apps--fixture (filename)
+ "Read fixture FILENAME from the fixtures directory."
+ (with-temp-buffer
+ (insert-file-contents (expand-file-name (concat "fixtures/" filename) test-sink-apps--dir))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording--get-sink-apps-normal-single-app ()
+ "Test parsing a single app on a single sink."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) (test-sink-apps--fixture "pactl-sink-inputs-active.txt"))))
+ (let ((result (cj/recording--get-sink-apps)))
+ (should (= 1 (length result)))
+ (should (equal '("Firefox") (cdr (assoc "65" result)))))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-normal-different-sink ()
+ "Test that sink index is correctly parsed from different fixture."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) (test-sink-apps--fixture "pactl-sink-inputs-different-sink.txt"))))
+ (let ((result (cj/recording--get-sink-apps)))
+ (should (= 1 (length result)))
+ (should (assoc "73" result))
+ (should-not (assoc "65" result)))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-normal-multiple-sinks ()
+ "Test parsing apps across multiple sinks."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd)
+ (concat (test-sink-apps--fixture "pactl-sink-inputs-active.txt")
+ "\n"
+ (test-sink-apps--fixture "pactl-sink-inputs-different-sink.txt")))))
+ (let ((result (cj/recording--get-sink-apps)))
+ (should (= 2 (length result)))
+ (should (assoc "65" result))
+ (should (assoc "73" result)))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-normal-multiple-apps-same-sink ()
+ "Test that multiple apps on the same sink are collected together."
+ (let ((output "Sink Input #1\n\tSink: 65\n\tProperties:\n\t\tapplication.name = \"Firefox\"\nSink Input #2\n\tSink: 65\n\tProperties:\n\t\tapplication.name = \"Spotify\"\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording--get-sink-apps)))
+ (should (= 1 (length result)))
+ (should (equal '("Firefox" "Spotify") (cdr (assoc "65" result))))))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-normal-deduplicates ()
+ "Test that duplicate app names on the same sink are deduplicated."
+ (let ((output "Sink Input #1\n\tSink: 65\n\tProperties:\n\t\tapplication.name = \"Firefox\"\nSink Input #2\n\tSink: 65\n\tProperties:\n\t\tapplication.name = \"Firefox\"\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (let ((result (cj/recording--get-sink-apps)))
+ (should (equal '("Firefox") (cdr (assoc "65" result))))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording--get-sink-apps-boundary-empty ()
+ "Test that empty pactl output returns empty alist."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (should (null (cj/recording--get-sink-apps)))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-boundary-no-properties ()
+ "Test sink input with no properties section returns empty."
+ (let ((output "Sink Input #1\n\tSink: 65\n\tDriver: PipeWire\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ (should (null (cj/recording--get-sink-apps))))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-boundary-no-sink-line ()
+ "Test sink input with no Sink: line does not crash."
+ (let ((output "Sink Input #1\n\tDriver: PipeWire\n\tProperties:\n\t\tapplication.name = \"Firefox\"\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ ;; current-sink is nil so app name won't be stored
+ (should (null (cj/recording--get-sink-apps))))))
+
+;;; Error Cases
+
+(ert-deftest test-video-audio-recording--get-sink-apps-error-garbled-output ()
+ "Test that garbled output does not crash, returns empty."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "random garbage\nwith\nnewlines\n")))
+ (should (null (cj/recording--get-sink-apps)))))
+
+(ert-deftest test-video-audio-recording--get-sink-apps-error-missing-app-name ()
+ "Test sink input with application.name missing value."
+ (let ((output "Sink Input #1\n\tSink: 65\n\tProperties:\n\t\tapplication.name = \"\"\n"))
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) output)))
+ ;; Empty string between quotes won't match [^\"]+ so no app stored
+ (should (null (cj/recording--get-sink-apps))))))
+
+(provide 'test-video-audio-recording--get-sink-apps)
+;;; test-video-audio-recording--get-sink-apps.el ends here
diff --git a/tests/test-video-audio-recording--label-devices.el b/tests/test-video-audio-recording--label-devices.el
new file mode 100644
index 00000000..e7ce70fb
--- /dev/null
+++ b/tests/test-video-audio-recording--label-devices.el
@@ -0,0 +1,110 @@
+;;; test-video-audio-recording--label-devices.el --- Tests for device labeling -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording--label-devices.
+;; Verifies label formatting ("Description [status]") and sort order
+;; (in use → ready → available → muted).
+;; Also indirectly tests cj/recording--device-sort-key and
+;; cj/recording--device-status-label since they are trivial helpers
+;; consumed entirely by label-devices.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording--label-devices-normal-format ()
+ "Test that labels are formatted as \"Description [status]\"."
+ (let ((result (cj/recording--label-devices
+ '(("sink-a" "JDS Labs" "RUNNING" "no")))))
+ (should (= 1 (length result)))
+ (should (equal "JDS Labs [in use]" (caar result)))
+ (should (equal "sink-a" (cdar result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-normal-sort-order ()
+ "Test that devices sort: in use → ready → available → muted."
+ (let ((result (cj/recording--label-devices
+ '(("d-suspended" "Suspended" "SUSPENDED" "no")
+ ("d-muted" "Muted" "RUNNING" "yes")
+ ("d-running" "Running" "RUNNING" "no")
+ ("d-idle" "Idle" "IDLE" "no")))))
+ (should (equal '("d-running" "d-idle" "d-suspended" "d-muted")
+ (mapcar #'cdr result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-normal-muted-label ()
+ "Test that muted devices get [muted] regardless of state."
+ (let ((result (cj/recording--label-devices
+ '(("m1" "Mic" "RUNNING" "yes")))))
+ (should (string-match-p "\\[muted\\]" (caar result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-normal-idle-label ()
+ "Test that IDLE unmuted devices get [ready]."
+ (let ((result (cj/recording--label-devices
+ '(("d1" "Device" "IDLE" "no")))))
+ (should (string-match-p "\\[ready\\]" (caar result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-normal-suspended-label ()
+ "Test that SUSPENDED unmuted devices get [available]."
+ (let ((result (cj/recording--label-devices
+ '(("d1" "Device" "SUSPENDED" "no")))))
+ (should (string-match-p "\\[available\\]" (caar result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-normal-returns-alist ()
+ "Test that result is a proper (label . name) alist."
+ (let ((result (cj/recording--label-devices
+ '(("a" "Alpha" "IDLE" "no")
+ ("b" "Beta" "RUNNING" "no")))))
+ (should (= 2 (length result)))
+ (dolist (entry result)
+ (should (consp entry))
+ (should (stringp (car entry)))
+ (should (stringp (cdr entry))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording--label-devices-boundary-empty ()
+ "Test that empty device list returns empty alist."
+ (should (null (cj/recording--label-devices nil))))
+
+(ert-deftest test-video-audio-recording--label-devices-boundary-single ()
+ "Test that single device returns single-element alist."
+ (let ((result (cj/recording--label-devices
+ '(("only" "Only Device" "IDLE" "no")))))
+ (should (= 1 (length result)))
+ (should (equal "only" (cdar result)))))
+
+(ert-deftest test-video-audio-recording--label-devices-boundary-all-muted ()
+ "Test that all-muted devices sort together and all get [muted]."
+ (let ((result (cj/recording--label-devices
+ '(("a" "A" "RUNNING" "yes")
+ ("b" "B" "IDLE" "yes")
+ ("c" "C" "SUSPENDED" "yes")))))
+ (should (= 3 (length result)))
+ (dolist (entry result)
+ (should (string-match-p "\\[muted\\]" (car entry))))))
+
+(ert-deftest test-video-audio-recording--label-devices-boundary-all-same-status ()
+ "Test that devices with identical status preserve relative order."
+ (let ((result (cj/recording--label-devices
+ '(("a" "Alpha" "RUNNING" "no")
+ ("b" "Beta" "RUNNING" "no")))))
+ (should (= 2 (length result)))
+ ;; Both should have [in use] label
+ (dolist (entry result)
+ (should (string-match-p "\\[in use\\]" (car entry))))))
+
+(ert-deftest test-video-audio-recording--label-devices-boundary-unknown-state ()
+ "Test that unknown state falls through to [available] (sort key 2)."
+ (let ((result (cj/recording--label-devices
+ '(("d1" "Device" "UNKNOWN_STATE" "no")))))
+ (should (string-match-p "\\[available\\]" (caar result)))))
+
+(provide 'test-video-audio-recording--label-devices)
+;;; test-video-audio-recording--label-devices.el ends here
diff --git a/tests/test-video-audio-recording--label-sinks.el b/tests/test-video-audio-recording--label-sinks.el
new file mode 100644
index 00000000..6bec9349
--- /dev/null
+++ b/tests/test-video-audio-recording--label-sinks.el
@@ -0,0 +1,111 @@
+;;; test-video-audio-recording--label-sinks.el --- Tests for sink labeling -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording--label-sinks.
+;; Verifies that sink labels include app names for active sinks,
+;; e.g. "JDS Labs [in use] (Firefox)" and sort correctly.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+(require 'video-audio-recording)
+
+;;; Helpers
+
+(defun test-label-sinks--stub (sink-apps-alist sinks-short-output)
+ "Return a cl-letf binding list that stubs get-sink-apps and sinks short.
+SINK-APPS-ALIST is the return value for `cj/recording--get-sink-apps'.
+SINKS-SHORT-OUTPUT is the raw pactl list sinks short output."
+ ;; We use cl-letf in each test to stub both get-sink-apps and
+ ;; shell-command-to-string (for the sinks short call inside label-sinks).
+ `(((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () ',sink-apps-alist))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ,sinks-short-output))))
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording--label-sinks-normal-appends-app-names ()
+ "Test that active sinks show app names in parentheses."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () '(("65" "Firefox"))))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "65\tjds-labs\tmodule\ts16le\tRUNNING\n")))
+ (let ((result (cj/recording--label-sinks
+ '(("jds-labs" "JDS Labs" "RUNNING" "no")))))
+ (should (= 1 (length result)))
+ (should (string-match-p "JDS Labs \\[in use\\] (Firefox)" (caar result))))))
+
+(ert-deftest test-video-audio-recording--label-sinks-normal-multiple-apps ()
+ "Test that multiple apps are comma-separated."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () '(("65" "Firefox" "Spotify"))))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "65\tjds-labs\tmodule\ts16le\tRUNNING\n")))
+ (let ((result (cj/recording--label-sinks
+ '(("jds-labs" "JDS Labs" "RUNNING" "no")))))
+ (should (string-match-p "(Firefox, Spotify)" (caar result))))))
+
+(ert-deftest test-video-audio-recording--label-sinks-normal-no-apps ()
+ "Test that sinks without apps have no parenthesized suffix."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "65\tjds-labs\tmodule\ts16le\tSUSPENDED\n")))
+ (let ((result (cj/recording--label-sinks
+ '(("jds-labs" "JDS Labs" "SUSPENDED" "no")))))
+ (should (string-match-p "JDS Labs \\[available\\]$" (caar result))))))
+
+(ert-deftest test-video-audio-recording--label-sinks-normal-sort-order ()
+ "Test that sinks sort: in use → ready → available → muted."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () '(("65" "Firefox"))))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd)
+ "65\tsink-run\tmodule\ts16le\tRUNNING\n73\tsink-idle\tmodule\ts16le\tIDLE\n80\tsink-sus\tmodule\ts16le\tSUSPENDED\n")))
+ (let ((result (cj/recording--label-sinks
+ '(("sink-sus" "Suspended" "SUSPENDED" "no")
+ ("sink-run" "Running" "RUNNING" "no")
+ ("sink-idle" "Idle" "IDLE" "no")))))
+ (should (equal '("sink-run" "sink-idle" "sink-sus")
+ (mapcar #'cdr result))))))
+
+(ert-deftest test-video-audio-recording--label-sinks-normal-muted-sink ()
+ "Test that muted sinks get [muted] label and no apps shown."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "65\tmuted-sink\tmodule\ts16le\tSUSPENDED\n")))
+ (let ((result (cj/recording--label-sinks
+ '(("muted-sink" "Muted Sink" "SUSPENDED" "yes")))))
+ (should (string-match-p "\\[muted\\]" (caar result))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording--label-sinks-boundary-empty ()
+ "Test that empty sink list returns empty alist."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (should (null (cj/recording--label-sinks nil)))))
+
+(ert-deftest test-video-audio-recording--label-sinks-boundary-sink-not-in-short ()
+ "Test that sink not found in sinks short still gets label (no apps)."
+ (cl-letf (((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () '(("65" "Firefox"))))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "")))
+ (let ((result (cj/recording--label-sinks
+ '(("unknown-sink" "Unknown" "IDLE" "no")))))
+ (should (= 1 (length result)))
+ ;; No app suffix since index lookup returned nil
+ (should (string-match-p "Unknown \\[ready\\]$" (caar result))))))
+
+(provide 'test-video-audio-recording--label-sinks)
+;;; test-video-audio-recording--label-sinks.el ends here