diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-06 13:00:01 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-06 13:12:44 -0600 |
| commit | c603124f6487604baee5aab590e1432e99570ca8 (patch) | |
| tree | 85c8b3ac4dc0573b1aee4448c1ae7d2c21ce3225 /tests | |
| parent | 25a2acb634212455abeb0a0c8fb1a97c3ece3a2c (diff) | |
feat(recording): rewrite device setup, fix video stop, update modeline icons
Video stop fix: kill wf-recorder (producer) first on Wayland so ffmpeg
gets clean EOF, then signal process group. Replaces sit-for with
poll-based wait-for-exit. Fixes zero-byte output files.
Device selection: rewrite quick setup to show all available mics with
PulseAudio descriptions, auto-detect default sink monitor for system
audio. Skip confirmation dialog, add Cancel option to mic list.
Modeline: replace red dot emoji with nerd font icons (mic/camcorder).
Rename quick-setup-for-calls to quick-setup, rebind C-; r s / C-; r S.
173 recording tests pass (was 165).
Diffstat (limited to 'tests')
16 files changed, 764 insertions, 346 deletions
diff --git a/tests/test-integration-recording-modeline-sync.el b/tests/test-integration-recording-modeline-sync.el index fab442bd..6bb0e99e 100644 --- a/tests/test-integration-recording-modeline-sync.el +++ b/tests/test-integration-recording-modeline-sync.el @@ -68,7 +68,7 @@ When user toggles audio recording on: 1. Process is created -2. Modeline indicator updates to show 🔴Audio +2. Modeline indicator updates to show (mic icon) 3. State is in sync immediately (not delayed) Components integrated: @@ -94,7 +94,7 @@ Validates: (cj/audio-recording-toggle nil) ;; Immediately after toggle: modeline should show recording - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Process should be alive (should (process-live-p cj/audio-recording-ffmpeg-process))) @@ -127,7 +127,7 @@ Validates: ;; Start recording (cj/audio-recording-toggle nil) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop recording (cj/audio-recording-toggle nil) @@ -150,7 +150,7 @@ Components integrated: Validates: - Video recording follows same sync pattern as audio -- Modeline shows 🔴Video correctly" +- Modeline shows correctly" (test-integration-modeline-setup) (unwind-protect (cl-letf (((symbol-function 'file-directory-p) @@ -164,7 +164,7 @@ Validates: ;; Start video (cj/video-recording-toggle nil) - (should (equal " 🔴Video " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop video (cj/video-recording-toggle nil) @@ -201,15 +201,15 @@ Validates: ;; State 2: Audio only (cj/audio-recording-toggle nil) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; State 3: Both (cj/video-recording-toggle nil) - (should (equal " 🔴A+V " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; State 4: Video only (stop audio) (cj/audio-recording-toggle nil) - (should (equal " 🔴Video " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; State 5: None (stop video) (cj/video-recording-toggle nil) @@ -249,7 +249,7 @@ Validates: (cj/audio-recording-toggle nil) ;; Immediately after start: should show recording - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Wait for process to exit and sentinel to run (sit-for 0.3) @@ -323,7 +323,7 @@ Validates: (dotimes (_i 5) ;; Start (cj/audio-recording-toggle nil) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) (should cj/audio-recording-ffmpeg-process) ;; Stop @@ -357,23 +357,23 @@ Validates: ;; Start audio (cj/audio-recording-toggle nil) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Add video - modeline should combine (cj/video-recording-toggle nil) - (should (equal " 🔴A+V " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop audio - video indicator should persist (cj/audio-recording-toggle nil) - (should (equal " 🔴Video " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Start audio again - should recombine (cj/audio-recording-toggle nil) - (should (equal " 🔴A+V " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop video - audio indicator should persist (cj/video-recording-toggle nil) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop audio - should be empty (cj/audio-recording-toggle nil) diff --git a/tests/test-integration-recording-monitor-capture-interactive.el b/tests/test-integration-recording-monitor-capture-interactive.el index ece8b79e..eb7cbf26 100644 --- a/tests/test-integration-recording-monitor-capture-interactive.el +++ b/tests/test-integration-recording-monitor-capture-interactive.el @@ -8,10 +8,10 @@ ;; **INTERACTIVE TEST - Run from within Emacs** ;; ;; This test must be run from an interactive Emacs session where recording -;; devices are already configured (C-; r c). +;; devices are already configured (C-; r s). ;; ;; USAGE: -;; 1. Ensure devices are configured: C-; r c +;; 1. Ensure devices are configured: C-; r s ;; 2. Load this file: M-x load-file RET tests/test-integration-recording-monitor-capture-interactive.el RET ;; 3. Run test: M-x test-recording-monitor-now RET ;; @@ -79,7 +79,7 @@ This function can be called with M-x to test recording without ERT framework." (unless (file-exists-p test-recording--test-audio) (user-error "Test audio file not found: %s" test-recording--test-audio)) (unless (and cj/recording-mic-device cj/recording-system-device) - (user-error "Recording devices not configured. Run C-; r c first")) + (user-error "Recording devices not configured. Run C-; r s first")) (let ((test-dir (make-temp-file "recording-test-" t)) (recording-file nil) diff --git a/tests/test-integration-recording-monitor-capture.el b/tests/test-integration-recording-monitor-capture.el index 7d7c5dfb..9a0c6380 100644 --- a/tests/test-integration-recording-monitor-capture.el +++ b/tests/test-integration-recording-monitor-capture.el @@ -16,7 +16,7 @@ ;; ;; Requirements: ;; - Audio system must be working (PulseAudio/PipeWire) -;; - Recording devices must be configured (C-; r c) +;; - Recording devices must be configured (C-; r s) ;; - paplay must be available ;; - Transcription backend must be configured ;; diff --git a/tests/test-integration-recording-toggle-workflow.el b/tests/test-integration-recording-toggle-workflow.el index c61698c5..0e000b68 100644 --- a/tests/test-integration-recording-toggle-workflow.el +++ b/tests/test-integration-recording-toggle-workflow.el @@ -10,7 +10,7 @@ ;; - cj/audio-recording-toggle (entry point) ;; - cj/video-recording-toggle (entry point) ;; - cj/recording-get-devices (device prompting and setup) -;; - cj/recording-quick-setup-for-calls (device selection workflow) +;; - cj/recording-quick-setup (device selection workflow) ;; - cj/ffmpeg-record-audio (process creation and ffmpeg command) ;; - cj/ffmpeg-record-video (process creation and ffmpeg command) ;; - cj/recording-modeline-indicator (UI state display) @@ -80,7 +80,7 @@ When user presses C-; r a for the first time: Components integrated: - cj/audio-recording-toggle (toggles start/stop) - cj/recording-get-devices (prompts for setup on first use) -- cj/recording-quick-setup-for-calls (device selection) +- cj/recording-quick-setup (device selection) - cj/ffmpeg-record-audio (creates recording process) - cj/recording-modeline-indicator (UI state) - cj/audio-recording-stop (cleanup) @@ -96,10 +96,8 @@ Validates: (let ((setup-called nil) (ffmpeg-cmd nil) (process-created nil)) - ;; Mock the device setup to simulate user choosing quick setup - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) ; User says yes to quick setup - ((symbol-function 'cj/recording-quick-setup-for-calls) + ;; Mock the device setup to simulate quick setup + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () (setq setup-called t) (setq cj/recording-mic-device "test-mic") @@ -130,7 +128,7 @@ Validates: (should (string-match-p "test-monitor" ffmpeg-cmd)) ;; Verify modeline shows recording - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; STEP 2: Second toggle - should stop recording (cj/audio-recording-toggle nil) @@ -168,7 +166,7 @@ Validates: (let ((setup-called nil) (ffmpeg-cmd nil)) - (cl-letf (((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () (setq setup-called t))) ((symbol-function 'file-directory-p) (lambda (_dir) t)) @@ -207,9 +205,7 @@ Validates: (test-integration-toggle-setup) (unwind-protect (let ((setup-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () (setq setup-called t) (setq cj/recording-mic-device "test-mic") @@ -226,7 +222,7 @@ Validates: ;; Verify setup and recording (should setup-called) (should cj/video-recording-ffmpeg-process) - (should (equal " 🔴Video " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop recording (cj/video-recording-toggle nil) @@ -271,7 +267,7 @@ Validates: ;; Verify both are recording (should cj/audio-recording-ffmpeg-process) (should cj/video-recording-ffmpeg-process) - (should (equal " 🔴A+V " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop audio only (cj/audio-recording-toggle nil) @@ -279,7 +275,7 @@ Validates: ;; Verify only video still recording (should (null cj/audio-recording-ffmpeg-process)) (should cj/video-recording-ffmpeg-process) - (should (equal " 🔴Video " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Stop video (cj/video-recording-toggle nil) @@ -329,7 +325,7 @@ Validates: ;; Verify recording started (should cj/audio-recording-ffmpeg-process) - (should (equal " 🔴Audio " (cj/recording-modeline-indicator))) + (should (equal " " (cj/recording-modeline-indicator))) ;; Wait for process to exit (sentinel should run) (sit-for 0.3) diff --git a/tests/test-video-audio-recording--get-available-mics.el b/tests/test-video-audio-recording--get-available-mics.el new file mode 100644 index 00000000..2fea0d7a --- /dev/null +++ b/tests/test-video-audio-recording--get-available-mics.el @@ -0,0 +1,102 @@ +;;; test-video-audio-recording--get-available-mics.el --- Tests for mic discovery -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/recording--get-available-mics. +;; Verifies that available microphones are discovered correctly: +;; - Monitor sources are excluded (they capture output, not input) +;; - Muted sources are excluded +;; - Friendly descriptions from PulseAudio are used + +;;; Code: + +(require 'ert) + +;; Stub dependencies before loading the module +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") + +(require 'video-audio-recording) + +;;; Helper + +(defun test-mics--make-pactl-output (sources) + "Build fake `pactl list sources' output from SOURCES. +Each source is (name description mute state)." + (mapconcat (lambda (src) + (format "Source #1\n\tState: %s\n\tName: %s\n\tDescription: %s\n\tMute: %s\n" + (nth 3 src) (nth 0 src) (nth 1 src) (nth 2 src))) + sources "")) + +;;; Normal Cases + +(ert-deftest test-video-audio-recording--get-available-mics-normal-filters-monitors () + "Test that monitor sources are excluded from mic list." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("alsa_input.usb-Jabra.mono" "Jabra Mono" "no" "SUSPENDED") + ("alsa_output.usb-Jabra.monitor" "Monitor of Jabra" "no" "SUSPENDED")))))) + (let ((mics (cj/recording--get-available-mics))) + (should (= 1 (length mics))) + (should (equal "alsa_input.usb-Jabra.mono" (car (car mics))))))) + +(ert-deftest test-video-audio-recording--get-available-mics-normal-filters-muted () + "Test that muted sources are excluded from mic list." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("active-mic" "Active Mic" "no" "SUSPENDED") + ("muted-mic" "Muted Mic" "yes" "SUSPENDED")))))) + (let ((mics (cj/recording--get-available-mics))) + (should (= 1 (length mics))) + (should (equal "active-mic" (car (car mics))))))) + +(ert-deftest test-video-audio-recording--get-available-mics-normal-uses-descriptions () + "Test that friendly descriptions are returned as cdr." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("raw-device-name" "Friendly Device Name" "no" "IDLE")))))) + (let ((mics (cj/recording--get-available-mics))) + (should (equal "Friendly Device Name" (cdr (car mics))))))) + +(ert-deftest test-video-audio-recording--get-available-mics-normal-multiple-mics () + "Test that multiple non-muted, non-monitor mics are returned." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("mic-a" "Jabra Mono" "no" "SUSPENDED") + ("mic-b" "Built-in" "no" "IDLE") + ("alsa_output.jabra.monitor" "Monitor of Jabra" "no" "SUSPENDED") + ("muted-mic" "Muted Mic" "yes" "SUSPENDED")))))) + (let ((mics (cj/recording--get-available-mics))) + (should (= 2 (length mics)))))) + +;;; Boundary Cases + +(ert-deftest test-video-audio-recording--get-available-mics-boundary-empty-output () + "Test that empty pactl output returns empty list." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) ""))) + (should (null (cj/recording--get-available-mics))))) + +(ert-deftest test-video-audio-recording--get-available-mics-boundary-all-monitors () + "Test that if all sources are monitors, returns empty list." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("sink-a.monitor" "Monitor A" "no" "SUSPENDED") + ("sink-b.monitor" "Monitor B" "no" "SUSPENDED")))))) + (should (null (cj/recording--get-available-mics))))) + +(ert-deftest test-video-audio-recording--get-available-mics-boundary-all-muted () + "Test that if all non-monitor sources are muted, returns empty list." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) + (test-mics--make-pactl-output + '(("muted-a" "Mic A" "yes" "SUSPENDED") + ("muted-b" "Mic B" "yes" "SUSPENDED")))))) + (should (null (cj/recording--get-available-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-default-sink-monitor.el b/tests/test-video-audio-recording--get-default-sink-monitor.el new file mode 100644 index 00000000..c5cf1abb --- /dev/null +++ b/tests/test-video-audio-recording--get-default-sink-monitor.el @@ -0,0 +1,56 @@ +;;; test-video-audio-recording--get-default-sink-monitor.el --- Tests for default sink monitor -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/recording--get-default-sink-monitor. +;; This function returns the monitor source name for the default audio +;; output, which captures "what you hear" (call audio, music, etc.). + +;;; 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--get-default-sink-monitor-normal-appends-monitor () + "Test that .monitor is appended to the default sink name." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) "alsa_output.usb-JDS_Labs-00.analog-stereo\n"))) + (should (equal "alsa_output.usb-JDS_Labs-00.analog-stereo.monitor" + (cj/recording--get-default-sink-monitor))))) + +(ert-deftest test-video-audio-recording--get-default-sink-monitor-normal-trims-whitespace () + "Test that trailing whitespace/newlines are stripped from sink name." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) " alsa_output.pci-0000.analog-stereo \n"))) + (should (equal "alsa_output.pci-0000.analog-stereo.monitor" + (cj/recording--get-default-sink-monitor))))) + +(ert-deftest test-video-audio-recording--get-default-sink-monitor-normal-bluetooth-sink () + "Test with a bluetooth default sink." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) "bluez_output.AA_BB_CC_DD_EE_FF.a2dp-sink\n"))) + (should (equal "bluez_output.AA_BB_CC_DD_EE_FF.a2dp-sink.monitor" + (cj/recording--get-default-sink-monitor))))) + +;;; Error Cases + +(ert-deftest test-video-audio-recording--get-default-sink-monitor-error-empty-output () + "Test that empty pactl output signals user-error." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) ""))) + (should-error (cj/recording--get-default-sink-monitor) :type 'user-error))) + +(ert-deftest test-video-audio-recording--get-default-sink-monitor-error-whitespace-only () + "Test that whitespace-only output signals user-error." + (cl-letf (((symbol-function 'shell-command-to-string) + (lambda (_cmd) " \n "))) + (should-error (cj/recording--get-default-sink-monitor) :type 'user-error))) + +(provide 'test-video-audio-recording--get-default-sink-monitor) +;;; test-video-audio-recording--get-default-sink-monitor.el ends here diff --git a/tests/test-video-audio-recording--parse-pactl-sources-verbose.el b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el new file mode 100644 index 00000000..e856f29a --- /dev/null +++ b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el @@ -0,0 +1,79 @@ +;;; test-video-audio-recording--parse-pactl-sources-verbose.el --- Tests for verbose pactl parser -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/recording--parse-pactl-sources-verbose. +;; Parses the verbose output of `pactl list sources' into structured tuples +;; of (name description mute state). + +;;; 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--parse-pactl-sources-verbose-normal-single-source () + "Test parsing a single source entry." + (let* ((output "Source #65\n\tState: SUSPENDED\n\tName: alsa_input.usb-Jabra-00.mono\n\tDescription: Jabra SPEAK 510 Mono\n\tMute: no\n") + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (= 1 (length result))) + (should (equal "alsa_input.usb-Jabra-00.mono" (nth 0 (car result)))) + (should (equal "Jabra SPEAK 510 Mono" (nth 1 (car result)))) + (should (equal "no" (nth 2 (car result)))) + (should (equal "SUSPENDED" (nth 3 (car result)))))) + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-multiple-sources () + "Test parsing multiple source entries." + (let* ((output (concat "Source #65\n\tState: SUSPENDED\n\tName: device-a\n\tDescription: Device A\n\tMute: no\n" + "Source #66\n\tState: RUNNING\n\tName: device-b\n\tDescription: Device B\n\tMute: yes\n")) + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (= 2 (length result))) + (should (equal "device-a" (nth 0 (car result)))) + (should (equal "Device B" (nth 1 (cadr result)))) + (should (equal "yes" (nth 2 (cadr result)))))) + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-monitors-included () + "Test that monitor sources are parsed (filtering is done by caller)." + (let* ((output "Source #67\n\tState: SUSPENDED\n\tName: alsa_output.jds.monitor\n\tDescription: Monitor of JDS Labs\n\tMute: no\n") + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (= 1 (length result))) + (should (string-match-p "\\.monitor$" (nth 0 (car result)))))) + +;;; Boundary Cases + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-empty-input () + "Test that empty input returns empty list." + (should (null (cj/recording--parse-pactl-sources-verbose "")))) + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-extra-fields () + "Test that extra fields between sources are ignored." + (let* ((output (concat "Source #65\n\tState: IDLE\n\tName: dev-a\n\tDescription: Dev A\n\tMute: no\n" + "\tDriver: PipeWire\n\tSample Specification: s16le 2ch 48000Hz\n" + "\tChannel Map: front-left,front-right\n" + "Source #66\n\tState: SUSPENDED\n\tName: dev-b\n\tDescription: Dev B\n\tMute: no\n")) + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (= 2 (length result))) + (should (equal "dev-a" (nth 0 (car result)))) + (should (equal "dev-b" (nth 0 (cadr result)))))) + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-description-with-parens () + "Test descriptions containing parentheses are captured fully." + (let* ((output "Source #91\n\tState: SUSPENDED\n\tName: hdmi.monitor\n\tDescription: Monitor of Radeon (HDMI 2)\n\tMute: no\n") + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (equal "Monitor of Radeon (HDMI 2)" (nth 1 (car result)))))) + +;;; Error Cases + +(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-error-malformed-no-name () + "Test that source entries without Name field are skipped." + (let* ((output "Source #65\n\tState: SUSPENDED\n\tDescription: Orphan\n\tMute: no\n") + (result (cj/recording--parse-pactl-sources-verbose output))) + (should (null result)))) + +(provide 'test-video-audio-recording--parse-pactl-sources-verbose) +;;; test-video-audio-recording--parse-pactl-sources-verbose.el ends here diff --git a/tests/test-video-audio-recording--wait-for-exit.el b/tests/test-video-audio-recording--wait-for-exit.el new file mode 100644 index 00000000..adacd828 --- /dev/null +++ b/tests/test-video-audio-recording--wait-for-exit.el @@ -0,0 +1,80 @@ +;;; test-video-audio-recording--wait-for-exit.el --- Tests for recording wait-for-exit -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for `cj/recording--wait-for-exit', the function that polls +;; for process exit instead of using a blind `sit-for' delay. +;; +;; This function is critical to the recording fix: without proper waiting, +;; ffmpeg cannot finalize container metadata, resulting in zero-byte files. +;; +;; Tests use real short-lived processes (`true', `sleep') to exercise the +;; actual polling logic rather than mocking `process-live-p'. + +;;; Code: + +(require 'ert) + +;; Stub dependencies before loading the module +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") + +;; Now load the actual production module +(require 'video-audio-recording) + +;;; Normal Cases + +(ert-deftest test-video-audio-recording--wait-for-exit-normal-process-exits-returns-t () + "Test that wait-for-exit returns t when process exits within timeout. +Uses `true' which exits immediately." + (let ((proc (make-process :name "test-exit" :command '("true")))) + (sit-for 0.2) ; Let it exit + (should (eq t (cj/recording--wait-for-exit proc 2))))) + +(ert-deftest test-video-audio-recording--wait-for-exit-normal-polls-until-exit () + "Test that wait-for-exit polls and returns t after process exits. +Uses a short sleep to verify polling works across multiple iterations." + (let ((proc (make-process :name "test-short" :command '("sleep" "0.2")))) + (should (eq t (cj/recording--wait-for-exit proc 3))))) + +;;; Boundary Cases + +(ert-deftest test-video-audio-recording--wait-for-exit-boundary-already-dead-returns-t () + "Test that wait-for-exit returns t immediately for already-dead process." + (let ((proc (make-process :name "test-dead" :command '("true")))) + (sit-for 0.2) ; Ensure it's dead + (should-not (process-live-p proc)) + (should (eq t (cj/recording--wait-for-exit proc 1))))) + +(ert-deftest test-video-audio-recording--wait-for-exit-boundary-zero-timeout-dead-process () + "Test that zero timeout with dead process returns t." + (let ((proc (make-process :name "test-zero-dead" :command '("true")))) + (sit-for 0.2) + (should (eq t (cj/recording--wait-for-exit proc 0))))) + +(ert-deftest test-video-audio-recording--wait-for-exit-boundary-zero-timeout-live-process () + "Test that zero timeout with live process returns nil (timed out)." + (let ((proc (make-process :name "test-zero-live" :command '("sleep" "1000")))) + (unwind-protect + (should (eq nil (cj/recording--wait-for-exit proc 0))) + (delete-process proc)))) + +(ert-deftest test-video-audio-recording--wait-for-exit-boundary-timeout-exceeded-returns-nil () + "Test that wait-for-exit returns nil when timeout is exceeded. +Uses a long-running process with a very short timeout." + (let ((proc (make-process :name "test-timeout" :command '("sleep" "1000")))) + (unwind-protect + (should (eq nil (cj/recording--wait-for-exit proc 0.2))) + (delete-process proc)))) + +;;; Error Cases + +(ert-deftest test-video-audio-recording--wait-for-exit-error-deleted-process-returns-t () + "Test that wait-for-exit handles a deleted (not just exited) process. +A process killed via `delete-process' should be detected as not live." + (let ((proc (make-process :name "test-deleted" :command '("sleep" "1000")))) + (delete-process proc) + (sit-for 0.1) + (should (eq t (cj/recording--wait-for-exit proc 1))))) + +(provide 'test-video-audio-recording--wait-for-exit) +;;; test-video-audio-recording--wait-for-exit.el ends here diff --git a/tests/test-video-audio-recording-ffmpeg-functions.el b/tests/test-video-audio-recording-ffmpeg-functions.el index e82614e2..a3bac0cf 100644 --- a/tests/test-video-audio-recording-ffmpeg-functions.el +++ b/tests/test-video-audio-recording-ffmpeg-functions.el @@ -166,17 +166,20 @@ ;;; Stop Functions - Normal Cases (ert-deftest test-video-audio-recording-video-stop-normal-interrupts-process () - "Test that stopping video recording interrupts the process." + "Test that stopping video recording sends SIGINT to process group." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) - (interrupt-called nil)) + (signal-called nil)) (setq cj/video-recording-ffmpeg-process fake-process) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (_proc) (setq interrupt-called t)))) + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil)) + ((symbol-function 'signal-process) + (lambda (_pid _sig) (setq signal-called t) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) (cj/video-recording-stop) - (should interrupt-called)) - (delete-process fake-process)) + (should signal-called)) + (ignore-errors (delete-process fake-process))) (test-ffmpeg-teardown))) (ert-deftest test-video-audio-recording-video-stop-normal-clears-variable () @@ -205,17 +208,19 @@ (test-ffmpeg-teardown))) (ert-deftest test-video-audio-recording-audio-stop-normal-interrupts-process () - "Test that stopping audio recording interrupts the process." + "Test that stopping audio recording sends SIGINT to process group." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000"))) - (interrupt-called nil)) + (signal-called nil)) (setq cj/audio-recording-ffmpeg-process fake-process) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (_proc) (setq interrupt-called t)))) + (cl-letf (((symbol-function 'signal-process) + (lambda (_pid _sig) (setq signal-called t) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) (cj/audio-recording-stop) - (should interrupt-called)) - (delete-process fake-process)) + (should signal-called)) + (ignore-errors (delete-process fake-process))) (test-ffmpeg-teardown))) (ert-deftest test-video-audio-recording-audio-stop-normal-clears-variable () @@ -257,70 +262,68 @@ ;;; Error Cases -(ert-deftest test-video-audio-recording-video-stop-error-interrupt-process-fails () - "Test that video stop handles interrupt-process failure gracefully." +(ert-deftest test-video-audio-recording-video-stop-error-signal-process-fails () + "Test that video stop handles signal-process failure gracefully." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) (error-raised nil)) (setq cj/video-recording-ffmpeg-process fake-process) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (_proc) (error "Interrupt failed")))) - ;; Should handle the error without crashing - (condition-case err + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil)) + ((symbol-function 'signal-process) + (lambda (_pid _sig) (error "Signal failed")))) + (condition-case _err (cj/video-recording-stop) (error (setq error-raised t))) - ;; Error should propagate (function doesn't catch it) (should error-raised)) - (delete-process fake-process)) + (ignore-errors (delete-process fake-process))) (test-ffmpeg-teardown))) -(ert-deftest test-video-audio-recording-audio-stop-error-interrupt-process-fails () - "Test that audio stop handles interrupt-process failure gracefully." +(ert-deftest test-video-audio-recording-audio-stop-error-signal-process-fails () + "Test that audio stop handles signal-process failure gracefully." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000"))) (error-raised nil)) (setq cj/audio-recording-ffmpeg-process fake-process) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (_proc) (error "Interrupt failed")))) - ;; Should handle the error without crashing - (condition-case err + (cl-letf (((symbol-function 'signal-process) + (lambda (_pid _sig) (error "Signal failed")))) + (condition-case _err (cj/audio-recording-stop) (error (setq error-raised t))) - ;; Error should propagate (function doesn't catch it) (should error-raised)) - (delete-process fake-process)) + (ignore-errors (delete-process fake-process))) (test-ffmpeg-teardown))) -(ert-deftest test-video-audio-recording-video-stop-error-dead-process-raises-error () - "Test that video stop raises error if process is already dead. -This documents current behavior - interrupt-process on dead process errors. -The sentinel should clear the variable before this happens in practice." +(ert-deftest test-video-audio-recording-video-stop-error-dead-process-handles-gracefully () + "Test that video stop handles already-dead process gracefully. +The new implementation guards against nil process-id, so stopping a dead +process should clean up without error." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000")))) (setq cj/video-recording-ffmpeg-process fake-process) - ;; Kill process before calling stop (delete-process fake-process) (sit-for 0.1) - ;; Calling stop on dead process raises error - (should-error (cj/video-recording-stop))) + ;; Should not error - process-id returns nil, guarded by `when pid' + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil))) + (cj/video-recording-stop)) + (should (null cj/video-recording-ffmpeg-process))) (test-ffmpeg-teardown))) -(ert-deftest test-video-audio-recording-audio-stop-error-dead-process-raises-error () - "Test that audio stop raises error if process is already dead. -This documents current behavior - interrupt-process on dead process errors. -The sentinel should clear the variable before this happens in practice." +(ert-deftest test-video-audio-recording-audio-stop-error-dead-process-handles-gracefully () + "Test that audio stop handles already-dead process gracefully. +The new implementation guards against nil process-id, so stopping a dead +process should clean up without error." (test-ffmpeg-setup) (unwind-protect (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000")))) (setq cj/audio-recording-ffmpeg-process fake-process) - ;; Kill process before calling stop (delete-process fake-process) (sit-for 0.1) - ;; Calling stop on dead process raises error - (should-error (cj/audio-recording-stop))) + ;; Should not error - process-id returns nil, guarded by `when pid' + (cj/audio-recording-stop) + (should (null cj/audio-recording-ffmpeg-process))) (test-ffmpeg-teardown))) (ert-deftest test-video-audio-recording-ffmpeg-record-video-boundary-skips-if-already-recording () diff --git a/tests/test-video-audio-recording-get-devices.el b/tests/test-video-audio-recording-get-devices.el index ba7d95b9..0af02bb3 100644 --- a/tests/test-video-audio-recording-get-devices.el +++ b/tests/test-video-audio-recording-get-devices.el @@ -4,9 +4,8 @@ ;; Unit tests for cj/recording-get-devices function. ;; Tests device prompting and validation workflow. ;; -;; NOTE: This function was refactored to use interactive prompts instead of -;; auto-detection. It now prompts the user with y-or-n-p and calls either -;; cj/recording-quick-setup-for-calls or cj/recording-select-devices. +;; NOTE: This function goes straight into quick setup (mic selection) +;; when devices aren't configured — no confirmation dialog. ;;; Code: @@ -46,29 +45,12 @@ (should (equal "preset-monitor" (cdr result))))) (test-get-devices-teardown))) -(ert-deftest test-video-audio-recording-get-devices-normal-prompts-when-not-configured () - "Test that function prompts user when devices not configured." - (test-get-devices-setup) - (unwind-protect - (let ((prompt-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) (setq prompt-called t) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) - (lambda () - (setq cj/recording-mic-device "quick-mic") - (setq cj/recording-system-device "quick-monitor")))) - (cj/recording-get-devices) - (should prompt-called))) - (test-get-devices-teardown))) - -(ert-deftest test-video-audio-recording-get-devices-normal-calls-quick-setup-on-yes () - "Test that function calls quick setup when user answers yes." +(ert-deftest test-video-audio-recording-get-devices-normal-calls-quick-setup () + "Test that function calls quick setup when devices not configured." (test-get-devices-setup) (unwind-protect (let ((quick-setup-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () (setq quick-setup-called t) (setq cj/recording-mic-device "quick-mic") @@ -77,29 +59,11 @@ (should quick-setup-called))) (test-get-devices-teardown))) -(ert-deftest test-video-audio-recording-get-devices-normal-calls-select-devices-on-no () - "Test that function calls manual selection when user answers no." - (test-get-devices-setup) - (unwind-protect - (let ((select-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) nil)) - ((symbol-function 'cj/recording-select-devices) - (lambda () - (setq select-called t) - (setq cj/recording-mic-device "manual-mic") - (setq cj/recording-system-device "manual-monitor")))) - (cj/recording-get-devices) - (should select-called))) - (test-get-devices-teardown))) - (ert-deftest test-video-audio-recording-get-devices-normal-returns-cons-cell () "Test that function returns (mic . monitor) cons cell." (test-get-devices-setup) (unwind-protect - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () (setq cj/recording-mic-device "test-mic") (setq cj/recording-system-device "test-monitor")))) @@ -111,40 +75,38 @@ ;;; Boundary Cases -(ert-deftest test-video-audio-recording-get-devices-boundary-only-mic-set-prompts () - "Test that function prompts even when only mic is set." +(ert-deftest test-video-audio-recording-get-devices-boundary-only-mic-set-calls-setup () + "Test that function calls setup even when only mic is set." (test-get-devices-setup) (unwind-protect (progn (setq cj/recording-mic-device "preset-mic") (setq cj/recording-system-device nil) - (let ((prompt-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) (setq prompt-called t) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (let ((quick-setup-called nil)) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () + (setq quick-setup-called t) (setq cj/recording-mic-device "new-mic") (setq cj/recording-system-device "new-monitor")))) (cj/recording-get-devices) - (should prompt-called)))) + (should quick-setup-called)))) (test-get-devices-teardown))) -(ert-deftest test-video-audio-recording-get-devices-boundary-only-monitor-set-prompts () - "Test that function prompts even when only monitor is set." +(ert-deftest test-video-audio-recording-get-devices-boundary-only-monitor-set-calls-setup () + "Test that function calls setup even when only monitor is set." (test-get-devices-setup) (unwind-protect (progn (setq cj/recording-mic-device nil) (setq cj/recording-system-device "preset-monitor") - (let ((prompt-called nil)) - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) (setq prompt-called t) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (let ((quick-setup-called nil)) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () + (setq quick-setup-called t) (setq cj/recording-mic-device "new-mic") (setq cj/recording-system-device "new-monitor")))) (cj/recording-get-devices) - (should prompt-called)))) + (should quick-setup-called)))) (test-get-devices-teardown))) ;;; Error Cases @@ -153,9 +115,7 @@ "Test that function signals error when setup fails to set devices." (test-get-devices-setup) (unwind-protect - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () nil))) ;; Setup fails - doesn't set devices (should-error (cj/recording-get-devices) :type 'user-error)) (test-get-devices-teardown))) @@ -164,27 +124,14 @@ "Test that error message guides user to setup commands." (test-get-devices-setup) (unwind-protect - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) t)) - ((symbol-function 'cj/recording-quick-setup-for-calls) + (cl-letf (((symbol-function 'cj/recording-quick-setup) (lambda () nil))) (condition-case err (cj/recording-get-devices) (user-error - (should (string-match-p "C-; r c" (error-message-string err))) + (should (string-match-p "C-; r s" (error-message-string err))) (should (string-match-p "C-; r s" (error-message-string err)))))) (test-get-devices-teardown))) -(ert-deftest test-video-audio-recording-get-devices-error-select-devices-fails () - "Test that function signals error when manual selection fails." - (test-get-devices-setup) - (unwind-protect - (cl-letf (((symbol-function 'y-or-n-p) - (lambda (_prompt) nil)) - ((symbol-function 'cj/recording-select-devices) - (lambda () nil))) ;; Manual selection fails - (should-error (cj/recording-get-devices) :type 'user-error)) - (test-get-devices-teardown))) - (provide 'test-video-audio-recording-get-devices) ;;; test-video-audio-recording-get-devices.el ends here diff --git a/tests/test-video-audio-recording-modeline-indicator.el b/tests/test-video-audio-recording-modeline-indicator.el index f7f3bbff..f717af7f 100644 --- a/tests/test-video-audio-recording-modeline-indicator.el +++ b/tests/test-video-audio-recording-modeline-indicator.el @@ -45,7 +45,7 @@ (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000")))) (setq cj/audio-recording-ffmpeg-process fake-process) (let ((result (cj/recording-modeline-indicator))) - (should (equal " 🔴Audio " result))) + (should (equal " " result))) (delete-process fake-process)) (test-modeline-indicator-teardown))) @@ -56,7 +56,7 @@ (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000")))) (setq cj/video-recording-ffmpeg-process fake-process) (let ((result (cj/recording-modeline-indicator))) - (should (equal " 🔴Video " result))) + (should (equal " " result))) (delete-process fake-process)) (test-modeline-indicator-teardown))) @@ -69,7 +69,7 @@ (setq cj/audio-recording-ffmpeg-process audio-proc) (setq cj/video-recording-ffmpeg-process video-proc) (let ((result (cj/recording-modeline-indicator))) - (should (equal " 🔴A+V " result))) + (should (equal " " result))) (delete-process audio-proc) (delete-process video-proc)) (test-modeline-indicator-teardown))) @@ -115,7 +115,7 @@ (delete-process dead-proc) (sit-for 0.1) (let ((result (cj/recording-modeline-indicator))) - (should (equal " 🔴Video " result))) + (should (equal " " result))) (delete-process alive-proc)) (test-modeline-indicator-teardown))) diff --git a/tests/test-video-audio-recording-process-cleanup.el b/tests/test-video-audio-recording-process-cleanup.el index 10b17f2c..d1cd442c 100644 --- a/tests/test-video-audio-recording-process-cleanup.el +++ b/tests/test-video-audio-recording-process-cleanup.el @@ -1,11 +1,14 @@ ;;; test-video-audio-recording-process-cleanup.el --- Tests for recording process cleanup -*- lexical-binding: t; -*- ;;; Commentary: -;; Tests that verify proper cleanup of recording processes, especially -;; wf-recorder on Wayland which can become orphaned if not explicitly killed. +;; Tests that verify proper cleanup of recording processes. ;; -;; Unit tests verify pkill is called at the right times. -;; Integration tests verify no orphan processes remain after stop. +;; Key behaviors tested: +;; - SIGINT is sent to the process group (negative PID), not just the shell +;; - Video stop waits for the process to actually exit before declaring done +;; - On Wayland, wf-recorder is killed AFTER ffmpeg exits (safety net) +;; - On X11, pkill is not called (no wf-recorder involved) +;; - Starting a recording cleans up orphan wf-recorder from previous crashes ;;; Code: @@ -38,29 +41,89 @@ (setq cj/recording-mic-device nil) (setq cj/recording-system-device nil)) -;;; Unit Tests - Verify pkill is called +;;; Unit Tests - Signal delivery -(ert-deftest test-video-recording-stop-wayland-calls-pkill-wf-recorder () - "Test that stopping video on Wayland calls pkill to kill wf-recorder." +(ert-deftest test-video-recording-stop-sends-sigint-to-process-group () + "Test that stopping sends SIGINT to the process group via negative PID." (test-cleanup-setup) (unwind-protect (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) - (pkill-called nil) - (pkill-args nil)) + (signaled-pid nil) + (signaled-sig nil)) + (setq cj/video-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil)) + ((symbol-function 'signal-process) + (lambda (pid sig) + (setq signaled-pid pid) + (setq signaled-sig sig) + 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) + (cj/video-recording-stop) + ;; Should send signal 2 (SIGINT) to negative PID (process group) + (should signaled-pid) + (should (< signaled-pid 0)) + (should (= signaled-sig 2))) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + +;;; Unit Tests - Wayland wf-recorder cleanup + +(ert-deftest test-video-recording-stop-wayland-kills-wf-recorder-before-signal () + "Test that on Wayland, pkill wf-recorder is called BEFORE signal-process. +Orderly pipeline shutdown requires killing the producer (wf-recorder) first +so ffmpeg sees EOF on its video input pipe and starts finalizing the file." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) + (call-order nil)) (setq cj/video-recording-ffmpeg-process fake-process) (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () t)) ((symbol-function 'call-process) (lambda (program &rest args) (when (equal program "pkill") - (setq pkill-called t) - (setq pkill-args args)) + (push (cons 'pkill args) call-order)) + 0)) + ((symbol-function 'signal-process) + (lambda (_pid _sig) + (push 'signal call-order) 0)) - ((symbol-function 'interrupt-process) (lambda (_proc) nil))) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) (cj/video-recording-stop) - (should pkill-called) - ;; Should call: pkill -INT wf-recorder - (should (member "-INT" pkill-args)) - (should (member "wf-recorder" pkill-args))) + ;; call-order is reversed (push adds to front), so last element is first call + (let ((order (reverse call-order))) + ;; First call should be pkill (kill producer first) + (should (consp (car order))) + (should (eq 'pkill (caar order))) + ;; signal-process should come after pkill + (should (eq 'signal (cadr order))))) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + +(ert-deftest test-video-recording-stop-wayland-calls-pkill-with-correct-args () + "Test that Wayland stop calls pkill -INT wf-recorder." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) + (pkill-args-list nil)) + (setq cj/video-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () t)) + ((symbol-function 'call-process) + (lambda (program &rest args) + (when (equal program "pkill") + (push args pkill-args-list)) + 0)) + ((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) + (cj/video-recording-stop) + ;; Should call pkill at least once with -INT wf-recorder + (should pkill-args-list) + (should (cl-some (lambda (args) + (and (member "-INT" args) + (member "wf-recorder" args))) + pkill-args-list))) (ignore-errors (delete-process fake-process))) (test-cleanup-teardown))) @@ -77,13 +140,16 @@ (when (equal program "pkill") (setq pkill-called t)) 0)) - ((symbol-function 'interrupt-process) (lambda (_proc) nil))) + ((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) t))) (cj/video-recording-stop) - ;; Should NOT call pkill on X11 (should-not pkill-called)) (ignore-errors (delete-process fake-process))) (test-cleanup-teardown))) +;;; Unit Tests - Start cleans orphans + (ert-deftest test-video-recording-start-wayland-kills-orphans () "Test that starting video on Wayland kills orphan wf-recorder processes." (test-cleanup-setup) @@ -102,7 +168,6 @@ (lambda (_name _buffer _command) (make-process :name "fake-video" :command '("sleep" "1000"))))) (cj/ffmpeg-record-video video-recordings-dir) - ;; Should call pkill to clean up orphans before starting (should pkill-called) (should (member "-INT" pkill-args)) (should (member "wf-recorder" pkill-args)))) @@ -123,10 +188,95 @@ (lambda (_name _buffer _command) (make-process :name "fake-video" :command '("sleep" "1000"))))) (cj/ffmpeg-record-video video-recordings-dir) - ;; Should NOT call pkill on X11 (should-not pkill-called))) (test-cleanup-teardown))) +;;; Unit Tests - Wait for exit + +(ert-deftest test-video-recording-stop-waits-for-process-exit () + "Test that stop waits for the process to exit instead of blind sit-for." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) + (wait-called nil) + (wait-timeout nil)) + (setq cj/video-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil)) + ((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc timeout) + (setq wait-called t) + (setq wait-timeout timeout) + t))) + (cj/video-recording-stop) + (should wait-called) + ;; Video uses 5 second timeout for MKV finalization + (should (= wait-timeout 5))) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + +(ert-deftest test-video-recording-stop-timeout-shows-warning () + "Test that video stop shows warning when process doesn't exit in time." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-video" :command '("sleep" "1000"))) + (warning-shown nil)) + (setq cj/video-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'cj/recording--wayland-p) (lambda () nil)) + ((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) nil)) ; Simulate timeout + ((symbol-function 'message) + (lambda (fmt &rest args) + (let ((msg (apply #'format fmt args))) + (when (string-match-p "did not exit" msg) + (setq warning-shown t)))))) + (cj/video-recording-stop) + (should warning-shown)) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + +(ert-deftest test-audio-recording-stop-timeout-shows-warning () + "Test that audio stop shows warning when process doesn't exit in time." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000"))) + (warning-shown nil)) + (setq cj/audio-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc _timeout) nil)) ; Simulate timeout + ((symbol-function 'message) + (lambda (fmt &rest args) + (let ((msg (apply #'format fmt args))) + (when (string-match-p "did not exit" msg) + (setq warning-shown t)))))) + (cj/audio-recording-stop) + (should warning-shown)) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + +(ert-deftest test-audio-recording-stop-waits-for-process-exit () + "Test that audio stop waits for the process to exit." + (test-cleanup-setup) + (unwind-protect + (let ((fake-process (make-process :name "test-audio" :command '("sleep" "1000"))) + (wait-called nil) + (wait-timeout nil)) + (setq cj/audio-recording-ffmpeg-process fake-process) + (cl-letf (((symbol-function 'signal-process) (lambda (_pid _sig) 0)) + ((symbol-function 'cj/recording--wait-for-exit) + (lambda (_proc timeout) + (setq wait-called t) + (setq wait-timeout timeout) + t))) + (cj/audio-recording-stop) + (should wait-called) + ;; Audio uses 3 second timeout for M4A finalization + (should (= wait-timeout 3))) + (ignore-errors (delete-process fake-process))) + (test-cleanup-teardown))) + ;;; Integration Tests - Verify actual process cleanup ;;; These tests require wf-recorder to be installed and a Wayland session @@ -150,21 +300,13 @@ This is an integration test that requires wf-recorder and Wayland." (test-cleanup-setup) (unwind-protect (let ((initial-count (test-cleanup--count-wf-recorder-processes))) - ;; Start recording (cj/ffmpeg-record-video video-recordings-dir) - (sit-for 1.0) ; Let it start - - ;; Should have at least one wf-recorder running + (sit-for 1.0) (should (> (test-cleanup--count-wf-recorder-processes) initial-count)) - - ;; Stop recording (cj/video-recording-stop) - (sit-for 1.0) ; Let cleanup complete - - ;; Should be back to initial count (no orphans) + (sit-for 1.0) (should (= (test-cleanup--count-wf-recorder-processes) initial-count))) (test-cleanup-teardown) - ;; Extra cleanup in case test failed mid-way (ignore-errors (call-process "pkill" nil nil nil "-INT" "wf-recorder")))) (ert-deftest test-integration-video-recording-start-cleans-orphans () @@ -179,28 +321,17 @@ This is an integration test that requires wf-recorder and Wayland." ;; Create an orphan wf-recorder (simulating a crash) (start-process "orphan-wf" nil "wf-recorder" "-c" "libx264" "-m" "matroska" "-f" "/dev/null") (sit-for 0.5) - (let ((orphan-pids (test-cleanup--get-wf-recorder-pids))) - (should orphan-pids) ; Verify orphan was created - - ;; Start recording - should clean up orphan + (should orphan-pids) (cj/ffmpeg-record-video video-recordings-dir) (sit-for 1.0) - - ;; The orphan PIDs should no longer exist (let ((current-pids (test-cleanup--get-wf-recorder-pids))) - ;; Old orphan should be gone (new recording process may exist) (dolist (orphan-pid orphan-pids) - ;; Check if old orphan is still in current pids - ;; It's OK if there's a NEW wf-recorder, just not the orphan (let ((still-running (member orphan-pid current-pids))) (should-not still-running)))) - - ;; Clean up the recording we started (cj/video-recording-stop) (sit-for 0.5))) (test-cleanup-teardown) - ;; Extra cleanup (ignore-errors (call-process "pkill" nil nil nil "-INT" "wf-recorder")))) (ert-deftest test-integration-video-recording-multiple-start-stop-cycles () @@ -212,17 +343,13 @@ This is an integration test that requires wf-recorder and Wayland." (test-cleanup-setup) (unwind-protect (let ((initial-count (test-cleanup--count-wf-recorder-processes))) - ;; Do 3 start/stop cycles (dotimes (_ 3) (cj/ffmpeg-record-video video-recordings-dir) (sit-for 0.5) (cj/video-recording-stop) (sit-for 0.5)) - - ;; Should be back to initial count - no accumulated orphans (should (= (test-cleanup--count-wf-recorder-processes) initial-count))) (test-cleanup-teardown) - ;; Extra cleanup (ignore-errors (call-process "pkill" nil nil nil "-INT" "wf-recorder")))) (provide 'test-video-audio-recording-process-cleanup) diff --git a/tests/test-video-audio-recording-quick-setup-for-calls.el b/tests/test-video-audio-recording-quick-setup-for-calls.el deleted file mode 100644 index 70980ad4..00000000 --- a/tests/test-video-audio-recording-quick-setup-for-calls.el +++ /dev/null @@ -1,144 +0,0 @@ -;;; test-video-audio-recording-quick-setup-for-calls.el --- Tests for cj/recording-quick-setup-for-calls -*- lexical-binding: t; -*- - -;;; Commentary: -;; Unit tests for cj/recording-quick-setup-for-calls function. -;; Tests quick device setup workflow for call recording. - -;;; Code: - -(require 'ert) - -;; Stub dependencies before loading the module -(defvar cj/custom-keymap (make-sparse-keymap) - "Stub keymap for testing.") - -;; Now load the actual production module -(require 'video-audio-recording) - -;;; Setup and Teardown - -(defun test-quick-setup-setup () - "Reset device variables before each test." - (setq cj/recording-mic-device nil) - (setq cj/recording-system-device nil)) - -(defun test-quick-setup-teardown () - "Clean up device variables after each test." - (setq cj/recording-mic-device nil) - (setq cj/recording-system-device nil)) - -;;; Normal Cases - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-normal-sets-both-devices () - "Test that function sets both mic and system device variables." - (test-quick-setup-setup) - (unwind-protect - (let ((grouped-devices '(("Bluetooth Headset" . ("bluez_input.00:1B:66" . "bluez_output.00_1B_66.monitor"))))) - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () grouped-devices)) - ((symbol-function 'completing-read) - (lambda (_prompt _choices &rest _args) "Bluetooth Headset"))) - (cj/recording-quick-setup-for-calls) - (should (equal "bluez_input.00:1B:66" cj/recording-mic-device)) - (should (equal "bluez_output.00_1B_66.monitor" cj/recording-system-device)))) - (test-quick-setup-teardown))) - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-normal-presents-friendly-names () - "Test that function presents friendly device names to user." - (test-quick-setup-setup) - (unwind-protect - (let ((grouped-devices '(("Jabra SPEAK 510 USB" . ("usb-input" . "usb-monitor")) - ("Built-in Audio" . ("pci-input" . "pci-monitor")))) - (presented-choices nil)) - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () grouped-devices)) - ((symbol-function 'completing-read) - (lambda (_prompt choices &rest _args) - (setq presented-choices choices) - (car choices)))) - (cj/recording-quick-setup-for-calls) - (should (member "Jabra SPEAK 510 USB" presented-choices)) - (should (member "Built-in Audio" presented-choices)))) - (test-quick-setup-teardown))) - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-normal-displays-confirmation () - "Test that function displays confirmation message with device details." - (test-quick-setup-setup) - (unwind-protect - (let ((grouped-devices '(("Bluetooth Headset" . ("bluez_input.00:1B:66" . "bluez_output.00_1B_66.monitor")))) - (message-text nil)) - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () grouped-devices)) - ((symbol-function 'completing-read) - (lambda (_prompt _choices &rest _args) "Bluetooth Headset")) - ((symbol-function 'message) - (lambda (fmt &rest args) (setq message-text (apply #'format fmt args))))) - (cj/recording-quick-setup-for-calls) - (should (string-match-p "Call recording ready" message-text)) - (should (string-match-p "Bluetooth Headset" message-text)))) - (test-quick-setup-teardown))) - -;;; Boundary Cases - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-boundary-single-device-no-prompt () - "Test that with single device, selection still happens." - (test-quick-setup-setup) - (unwind-protect - (let ((grouped-devices '(("Built-in Audio" . ("pci-input" . "pci-monitor"))))) - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () grouped-devices)) - ((symbol-function 'completing-read) - (lambda (_prompt _choices &rest _args) "Built-in Audio"))) - (cj/recording-quick-setup-for-calls) - (should (equal "pci-input" cj/recording-mic-device)) - (should (equal "pci-monitor" cj/recording-system-device)))) - (test-quick-setup-teardown))) - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-boundary-device-name-with-special-chars () - "Test that device names with special characters are handled correctly." - (test-quick-setup-setup) - (unwind-protect - (let ((grouped-devices '(("Device (USB-C)" . ("special-input" . "special-monitor"))))) - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () grouped-devices)) - ((symbol-function 'completing-read) - (lambda (_prompt _choices &rest _args) "Device (USB-C)"))) - (cj/recording-quick-setup-for-calls) - (should (equal "special-input" cj/recording-mic-device)) - (should (equal "special-monitor" cj/recording-system-device)))) - (test-quick-setup-teardown))) - -;;; Error Cases - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-error-no-devices-signals-error () - "Test that function signals user-error when no complete devices are found." - (test-quick-setup-setup) - (unwind-protect - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () nil))) - (should-error (cj/recording-quick-setup-for-calls) :type 'user-error)) - (test-quick-setup-teardown))) - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-error-message-mentions-both-devices () - "Test that error message mentions need for both mic and monitor." - (test-quick-setup-setup) - (unwind-protect - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () nil))) - (condition-case err - (cj/recording-quick-setup-for-calls) - (user-error - (should (string-match-p "both mic and monitor" (error-message-string err)))))) - (test-quick-setup-teardown))) - -(ert-deftest test-video-audio-recording-quick-setup-for-calls-error-empty-device-list () - "Test that empty device list from grouping is handled gracefully." - (test-quick-setup-setup) - (unwind-protect - (cl-letf (((symbol-function 'cj/recording-group-devices-by-hardware) - (lambda () '()))) - (should-error (cj/recording-quick-setup-for-calls) :type 'user-error)) - (test-quick-setup-teardown))) - -(provide 'test-video-audio-recording-quick-setup-for-calls) -;;; test-video-audio-recording-quick-setup-for-calls.el ends here diff --git a/tests/test-video-audio-recording-quick-setup.el b/tests/test-video-audio-recording-quick-setup.el new file mode 100644 index 00000000..8b46b9b0 --- /dev/null +++ b/tests/test-video-audio-recording-quick-setup.el @@ -0,0 +1,172 @@ +;;; test-video-audio-recording-quick-setup.el --- Tests for cj/recording-quick-setup -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/recording-quick-setup function. +;; The quick setup shows available mics and auto-selects the default +;; sink's monitor for system audio capture. + +;;; Code: + +(require 'ert) + +;; Stub dependencies before loading the module +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") + +;; Now load the actual production module +(require 'video-audio-recording) + +;;; Setup and Teardown + +(defun test-quick-setup-setup () + "Reset device variables before each test." + (setq cj/recording-mic-device nil) + (setq cj/recording-system-device nil)) + +(defun test-quick-setup-teardown () + "Clean up device variables after each test." + (setq cj/recording-mic-device nil) + (setq cj/recording-system-device nil)) + +;;; Normal Cases + +(ert-deftest test-video-audio-recording-quick-setup-normal-sets-mic-device () + "Test that selecting a mic sets cj/recording-mic-device." + (test-quick-setup-setup) + (unwind-protect + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("jabra-input" . "Jabra SPEAK 510 Mono") + ("builtin-input" . "Built-in Analog")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "jds-labs.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt table &rest _args) + (car (all-completions "" table))))) + (cj/recording-quick-setup) + (should (equal "jabra-input" cj/recording-mic-device))) + (test-quick-setup-teardown))) + +(ert-deftest test-video-audio-recording-quick-setup-normal-sets-system-to-default-monitor () + "Test that system device is set to the default sink's monitor." + (test-quick-setup-setup) + (unwind-protect + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("jabra-input" . "Jabra SPEAK 510 Mono")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "alsa_output.usb-JDS_Labs-00.analog-stereo.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt table &rest _args) + (car (all-completions "" table))))) + (cj/recording-quick-setup) + (should (equal "alsa_output.usb-JDS_Labs-00.analog-stereo.monitor" + cj/recording-system-device))) + (test-quick-setup-teardown))) + +(ert-deftest test-video-audio-recording-quick-setup-normal-presents-descriptions () + "Test that completing-read receives friendly descriptions and Cancel option." + (test-quick-setup-setup) + (unwind-protect + (let ((presented-candidates nil)) + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("raw-device-1" . "Jabra SPEAK 510 Mono") + ("raw-device-2" . "Built-in Analog")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "default.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt table &rest _args) + (setq presented-candidates (all-completions "" table)) + (car presented-candidates)))) + (cj/recording-quick-setup) + ;; Candidates should have friendly descriptions + (should (member "Jabra SPEAK 510 Mono" presented-candidates)) + (should (member "Built-in Analog" presented-candidates)) + ;; Cancel option should be present + (should (member "Cancel" presented-candidates)))) + (test-quick-setup-teardown))) + +(ert-deftest test-video-audio-recording-quick-setup-normal-confirmation-message () + "Test that confirmation message mentions the selected mic and monitor." + (test-quick-setup-setup) + (unwind-protect + (let ((message-text nil)) + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("jabra-input" . "Jabra SPEAK 510 Mono")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "jds-labs.analog-stereo.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt table &rest _args) + (car (all-completions "" table)))) + ((symbol-function 'message) + (lambda (fmt &rest args) + (setq message-text (apply #'format fmt args))))) + (cj/recording-quick-setup) + (should (string-match-p "Recording ready" message-text)) + (should (string-match-p "Jabra SPEAK 510 Mono" message-text)) + (should (string-match-p "default output monitor" message-text)))) + (test-quick-setup-teardown))) + +;;; Boundary Cases + +(ert-deftest test-video-audio-recording-quick-setup-boundary-single-mic () + "Test that with only one mic, it still presents selection." + (test-quick-setup-setup) + (unwind-protect + (let ((read-called nil)) + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("sole-mic" . "Only Mic Available")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "default.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt table &rest _args) + (setq read-called t) + (car (all-completions "" table))))) + (cj/recording-quick-setup) + (should read-called) + (should (equal "sole-mic" cj/recording-mic-device)))) + (test-quick-setup-teardown))) + +;;; Error Cases + +(ert-deftest test-video-audio-recording-quick-setup-error-cancel-selected () + "Test that selecting Cancel signals user-error and does not set devices." + (test-quick-setup-setup) + (unwind-protect + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () '(("jabra-input" . "Jabra SPEAK 510 Mono")))) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "default.monitor")) + ((symbol-function 'completing-read) + (lambda (_prompt _choices &rest _args) + "Cancel"))) + (should-error (cj/recording-quick-setup) :type 'user-error) + (should (null cj/recording-mic-device)) + (should (null cj/recording-system-device))) + (test-quick-setup-teardown))) + +(ert-deftest test-video-audio-recording-quick-setup-error-no-mics () + "Test that function signals error when no mics are found." + (test-quick-setup-setup) + (unwind-protect + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () nil)) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "default.monitor"))) + (should-error (cj/recording-quick-setup) :type 'user-error)) + (test-quick-setup-teardown))) + +(ert-deftest test-video-audio-recording-quick-setup-error-no-mics-message () + "Test that error message mentions mic and unmuted." + (test-quick-setup-setup) + (unwind-protect + (cl-letf (((symbol-function 'cj/recording--get-available-mics) + (lambda () nil)) + ((symbol-function 'cj/recording--get-default-sink-monitor) + (lambda () "default.monitor"))) + (condition-case err + (cj/recording-quick-setup) + (user-error + (should (string-match-p "mic" (error-message-string err)))))) + (test-quick-setup-teardown))) + +(provide 'test-video-audio-recording-quick-setup) +;;; test-video-audio-recording-quick-setup.el ends here diff --git a/tests/test-video-audio-recording-test-mic.el b/tests/test-video-audio-recording-test-mic.el index 5aa794bb..60b9eb0b 100644 --- a/tests/test-video-audio-recording-test-mic.el +++ b/tests/test-video-audio-recording-test-mic.el @@ -124,7 +124,7 @@ (condition-case err (cj/recording-test-mic) (user-error - (should (string-match-p "C-; r c" (error-message-string err)))))) + (should (string-match-p "C-; r s" (error-message-string err)))))) (test-mic-teardown))) (ert-deftest test-video-audio-recording-test-mic-error-ffmpeg-failure-handled () diff --git a/tests/test-video-audio-recording-test-monitor.el b/tests/test-video-audio-recording-test-monitor.el index f1476577..d821600f 100644 --- a/tests/test-video-audio-recording-test-monitor.el +++ b/tests/test-video-audio-recording-test-monitor.el @@ -125,7 +125,7 @@ (condition-case err (cj/recording-test-monitor) (user-error - (should (string-match-p "C-; r c" (error-message-string err)))))) + (should (string-match-p "C-; r s" (error-message-string err)))))) (test-monitor-teardown))) (ert-deftest test-video-audio-recording-test-monitor-error-ffmpeg-failure-handled () |
