aboutsummaryrefslogtreecommitdiff
path: root/tests/test-elfeed-config-helpers.el
blob: 59a0ed3310ee45656c66cd98e8d0cc450c7d8c8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
;;; test-elfeed-config-helpers.el --- Tests for elfeed stream/process helpers -*- lexical-binding: t; -*-

;;; Commentary:
;; Coverage for two elfeed-config helpers that were untested:
;;   - cj/extract-stream-url: runs yt-dlp -g to resolve a direct stream URL,
;;     returning the URL, nil on non-URL / nonzero exit, or signalling when
;;     yt-dlp is absent.
;;   - cj/elfeed-process-entries: applies an action to each selected entry,
;;     marking them read; errors when nothing is selected, skips entries with
;;     no link, and (by default) catches per-entry action errors.
;;
;; The cj/elfeed-process-entries tests build real `elfeed-entry' structs
;; rather than stubbing the `elfeed-entry-link' accessor.  A byte-compiled
;; cj/elfeed-process-entries inlines that accessor -- the cl-defstruct
;; compiler macro expands `(elfeed-entry-link e)' into an `elfeed-entry-p'
;; check plus an `aref' -- so a function stub is bypassed and the inlined
;; check type-rejects a fake entry.  Using genuine structs survives both the
;; interpreted and the byte-compiled load.  The elfeed-search UI boundary
;; (selection, tagging, redisplay) is still stubbed.  `skip-unless' guards
;; the struct-building tests so an environment without the elfeed package
;; skips rather than errors.

;;; Code:

(require 'ert)
(require 'cl-lib)

(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
;; Put the installed packages on the load-path so the real `elfeed-entry'
;; struct is available -- the process-entries tests build genuine structs
;; rather than fakes (see Commentary).  `skip-unless' covers the rare
;; environment where the package isn't installed.
(package-initialize)
(require 'elfeed-config)
(require 'elfeed nil t)

;;; cj/extract-stream-url

(ert-deftest test-elfeed-extract-stream-url-normal-returns-url ()
  "Normal: a successful yt-dlp run returns the trimmed https stream URL."
  (cl-letf (((symbol-function 'executable-find)
             (lambda (p) (and (equal p "yt-dlp") "/usr/bin/yt-dlp")))
            ((symbol-function 'cj/log-silently) #'ignore)
            ((symbol-function 'call-process)
             (lambda (_prog _infile _dest _disp &rest _args)
               (insert "https://stream.example/abc\n") 0)))
    (should (equal "https://stream.example/abc"
                   (cj/extract-stream-url "https://youtube.com/watch?v=x" "best")))))

(ert-deftest test-elfeed-extract-stream-url-boundary-non-url-output-is-nil ()
  "Boundary: output that is not an http(s) URL yields nil, not the raw text."
  (cl-letf (((symbol-function 'executable-find) (lambda (_) "/usr/bin/yt-dlp"))
            ((symbol-function 'cj/log-silently) #'ignore)
            ((symbol-function 'call-process)
             (lambda (_p _i _d _disp &rest _) (insert "ERROR: unavailable\n") 0)))
    (should (null (cj/extract-stream-url "u" nil)))))

(ert-deftest test-elfeed-extract-stream-url-boundary-nonzero-exit-is-nil ()
  "Boundary: a nonzero yt-dlp exit code yields nil."
  (cl-letf (((symbol-function 'executable-find) (lambda (_) "/usr/bin/yt-dlp"))
            ((symbol-function 'cj/log-silently) #'ignore)
            ((symbol-function 'call-process)
             (lambda (_p _i _d _disp &rest _) (insert "boom") 1)))
    (should (null (cj/extract-stream-url "u" nil)))))

(ert-deftest test-elfeed-extract-stream-url-error-without-yt-dlp ()
  "Error: a missing yt-dlp signals before attempting the call."
  (cl-letf (((symbol-function 'executable-find) (lambda (_) nil)))
    (should-error (cj/extract-stream-url "u" "best") :type 'error)))

;;; cj/elfeed-process-entries

(defun cj/test--elfeed-entry (link)
  "Return a real `elfeed-entry' whose link slot is LINK."
  (elfeed-entry--create :link link))

(ert-deftest test-elfeed-process-entries-normal-applies-action-and-marks-read ()
  "Normal: each selected entry's link is passed to the action and untagged."
  (skip-unless (featurep 'elfeed))
  (let* ((e1 (cj/test--elfeed-entry "http://e1"))
         (e2 (cj/test--elfeed-entry "http://e2"))
         (acted nil) (untagged nil))
    (cl-letf (((symbol-function 'elfeed-search-selected) (lambda (&rest _) (list e1 e2)))
              ((symbol-function 'elfeed-untag) (lambda (e &rest _) (push e untagged)))
              ((symbol-function 'elfeed-search-update-entry) #'ignore)
              ((symbol-function 'use-region-p) (lambda () t)))
      (cj/elfeed-process-entries (lambda (link) (push link acted)) "open")
      (should (equal '("http://e1" "http://e2") (nreverse acted)))
      (should (equal (list e1 e2) (nreverse untagged))))))

(ert-deftest test-elfeed-process-entries-error-no-selection ()
  "Error: no selected entries signals rather than silently doing nothing."
  (cl-letf (((symbol-function 'elfeed-search-selected) (lambda (&rest _) nil)))
    (should-error (cj/elfeed-process-entries #'ignore "open") :type 'error)))

(ert-deftest test-elfeed-process-entries-boundary-skips-entry-with-no-link ()
  "Boundary: an entry with no link is untagged but not passed to the action."
  (skip-unless (featurep 'elfeed))
  (let ((entry (cj/test--elfeed-entry nil))
        (acted 0))
    (cl-letf (((symbol-function 'elfeed-search-selected) (lambda (&rest _) (list entry)))
              ((symbol-function 'elfeed-untag) #'ignore)
              ((symbol-function 'elfeed-search-update-entry) #'ignore)
              ((symbol-function 'use-region-p) (lambda () t)))
      (cj/elfeed-process-entries (lambda (_) (cl-incf acted)) "open")
      (should (= 0 acted)))))

(ert-deftest test-elfeed-process-entries-error-default-catches-action-error ()
  "Error: by default a per-entry action error is caught (messaged), not raised."
  (skip-unless (featurep 'elfeed))
  (let ((entry (cj/test--elfeed-entry "http://e1")))
    (cl-letf (((symbol-function 'elfeed-search-selected) (lambda (&rest _) (list entry)))
              ((symbol-function 'elfeed-untag) #'ignore)
              ((symbol-function 'elfeed-search-update-entry) #'ignore)
              ((symbol-function 'use-region-p) (lambda () t))
              ((symbol-function 'message) #'ignore))
      ;; Returns normally despite the action erroring.
      (cj/elfeed-process-entries (lambda (_) (error "boom")) "open")
      (should t))))

(ert-deftest test-elfeed-process-entries-boundary-skip-error-handling-propagates ()
  "Boundary: with SKIP-ERROR-HANDLING, a per-entry action error propagates."
  (skip-unless (featurep 'elfeed))
  (let ((entry (cj/test--elfeed-entry "http://e1")))
    (cl-letf (((symbol-function 'elfeed-search-selected) (lambda (&rest _) (list entry)))
              ((symbol-function 'elfeed-untag) #'ignore)
              ((symbol-function 'elfeed-search-update-entry) #'ignore)
              ((symbol-function 'use-region-p) (lambda () t)))
      (should-error (cj/elfeed-process-entries (lambda (_) (error "boom")) "open" t)))))

(provide 'test-elfeed-config-helpers)
;;; test-elfeed-config-helpers.el ends here