diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-06 13:20:52 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-06 13:20:52 -0500 |
| commit | c60c8edf2e0e4dcdaab451d630b05d7df583bad6 (patch) | |
| tree | b1b88aee50663119986e19d1ae29d928e536ca77 /tests | |
| parent | ba1c418428f9d09f57736d66e61ae28974a91117 (diff) | |
| download | dotemacs-c60c8edf2e0e4dcdaab451d630b05d7df583bad6.tar.gz dotemacs-c60c8edf2e0e4dcdaab451d630b05d7df583bad6.zip | |
feat(music): radio-browser search client + emitter (phase 1)
I built the first phase of the radio-browser lookup: the pure pieces and the network client behind the search command, with no UI yet. It lives in modules/music-config.el, alongside the existing radio-station creator.
The client is url.el plus json-parse-string, trying a pinned host and falling back to one from /json/servers. The pure layer parses a response into station plists, and a non-JSON body signals a clear user-error rather than a stack trace. It picks a station's stream URL (url_resolved, then url), emits the .m3u in the existing radio format with the radio-browser UUID, formats the completion annotation (codec, bitrate, country, votes, tags), and disambiguates a colliding filename by UUID. Names are newline-stripped so an odd one can't inject extra m3u lines.
The pure pieces are unit-tested (14). The network client is smoke-tested against the live API.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-music-config--radio.el | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/tests/test-music-config--radio.el b/tests/test-music-config--radio.el new file mode 100644 index 00000000..651e2cb7 --- /dev/null +++ b/tests/test-music-config--radio.el @@ -0,0 +1,140 @@ +;;; test-music-config--radio.el --- radio-browser lookup pure-logic tests -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; Phase 1 of the radio-browser lookup (spec docs/specs/2026-07-06-radio-browser-lookup-spec.org): +;; the pure pieces behind the search command. Parsing a recorded JSON response, +;; picking a station's stream URL, emitting the .m3u, formatting the marginalia +;; annotation (Variant B: codec/bitrate/country/votes/tags), disambiguating a +;; colliding filename, and building the search URL. The network GET and the +;; interactive command are exercised in the daemon, not here. + +;;; Code: + +(require 'ert) + +;; Stub dependencies before loading the module. +(defvar cj/custom-keymap (make-sparse-keymap) + "Stub keymap for testing.") + +(require 'music-config) + +(declare-function cj/music-radio--parse-search "music-config" (json-text)) +(declare-function cj/music-radio--station-url "music-config" (st)) +(declare-function cj/music-radio--station-m3u "music-config" (st)) +(declare-function cj/music-radio--tags-snippet "music-config" (tags n)) +(declare-function cj/music-radio--format-candidate "music-config" (st)) +(declare-function cj/music-radio--disambiguate-name "music-config" (name uuid taken)) +(declare-function cj/music-radio--search-url "music-config" (server query)) + +(defconst test-music-radio--fixture + (concat "[{\"stationuuid\":\"ea8059be-d119-4de3-b27b-0d9bd6aedb17\"," + "\"name\":\"Adroit Jazz Underground\",\"url_resolved\":\"https://icecast.walmradio.com:8443/jazz\"," + "\"codec\":\"MP3\",\"bitrate\":320,\"countrycode\":\"US\",\"votes\":174208,\"tags\":\"bebop,hard bop,cool\"}," + "{\"stationuuid\":\"00000000-no-url\",\"name\":\"No URL Station\",\"url_resolved\":\"\",\"url\":\"\"," + "\"codec\":\"AAC\",\"bitrate\":0,\"countrycode\":\"FR\",\"votes\":5,\"tags\":\"\"}]") + "A two-station recorded radio-browser search response.") + +(defun test-music-radio--first () + "First station plist from the fixture." + (car (cj/music-radio--parse-search test-music-radio--fixture))) + +;;; --------------------------- parse-search ----------------------------------- + +(ert-deftest test-music-radio-parse-search-normal () + "Normal: a recorded response parses to station plists with the expected fields." + (let ((stations (cj/music-radio--parse-search test-music-radio--fixture))) + (should (= (length stations) 2)) + (should (equal (plist-get (car stations) :name) "Adroit Jazz Underground")) + (should (equal (plist-get (car stations) :stationuuid) + "ea8059be-d119-4de3-b27b-0d9bd6aedb17")))) + +(ert-deftest test-music-radio-parse-search-empty () + "Boundary: an empty result array parses to nil." + (should-not (cj/music-radio--parse-search "[]"))) + +(ert-deftest test-music-radio-parse-search-malformed-user-errors () + "Error: a non-JSON body (a gateway page) signals user-error, not a raw parse error." + (should-error (cj/music-radio--parse-search "<html>502 Bad Gateway</html>") + :type 'user-error)) + +;;; --------------------------- station-url ------------------------------------ + +(ert-deftest test-music-radio-station-url-resolved () + "Normal: url_resolved is preferred." + (should (equal (cj/music-radio--station-url (test-music-radio--first)) + "https://icecast.walmradio.com:8443/jazz"))) + +(ert-deftest test-music-radio-station-url-fallback-to-url () + "Boundary: empty url_resolved falls back to url." + (should (equal (cj/music-radio--station-url + '(:url_resolved "" :url "http://example.test/stream")) + "http://example.test/stream"))) + +(ert-deftest test-music-radio-station-url-none () + "Error: neither url_resolved nor url yields nil." + (should-not (cj/music-radio--station-url '(:url_resolved "" :url "")))) + +;;; --------------------------- station-m3u ------------------------------------ + +(ert-deftest test-music-radio-station-m3u-shape () + "Normal: the .m3u carries #EXTM3U, the UUID, #EXTINF, and the stream URL." + (let ((m3u (cj/music-radio--station-m3u (test-music-radio--first)))) + (should (string-prefix-p "#EXTM3U\n" m3u)) + (should (string-match-p "#RADIOBROWSERUUID:ea8059be-d119-4de3-b27b-0d9bd6aedb17" m3u)) + (should (string-match-p "#EXTINF:1,Adroit Jazz Underground" m3u)) + (should (string-match-p "https://icecast.walmradio.com:8443/jazz" m3u)))) + +(ert-deftest test-music-radio-station-m3u-no-url-is-nil () + "Error: a station with no usable stream URL emits nil (not a broken file)." + (should-not (cj/music-radio--station-m3u '(:name "Broken" :url_resolved "" :url "")))) + +;;; --------------------------- tags-snippet ----------------------------------- + +(ert-deftest test-music-radio-tags-snippet-takes-first-n () + "Normal: the first N comma-separated tags render trimmed." + (should (equal (cj/music-radio--tags-snippet "bebop, hard bop, cool, free jazz" 3) + "bebop, hard bop, cool"))) + +(ert-deftest test-music-radio-tags-snippet-empty () + "Boundary: empty or nil tags render as the empty string." + (should (equal (cj/music-radio--tags-snippet "" 3) "")) + (should (equal (cj/music-radio--tags-snippet nil 3) ""))) + +;;; --------------------------- format-candidate (Variant B) ------------------- + +(ert-deftest test-music-radio-format-candidate-variant-b () + "Normal: the annotation carries codec, votes, and tags (Variant B)." + (let ((ann (cj/music-radio--format-candidate (test-music-radio--first)))) + (should (string-match-p "MP3" ann)) + (should (string-match-p "174208" ann)) + (should (string-match-p "bebop" ann)))) + +;;; --------------------------- disambiguate-name ------------------------------ + +(ert-deftest test-music-radio-disambiguate-name-unique () + "Normal: a name not already taken keeps its safe basename." + (should (equal (cj/music-radio--disambiguate-name "Jazz Radio" "uuid1234" '()) + "Jazz_Radio"))) + +(ert-deftest test-music-radio-disambiguate-name-collision () + "Boundary: a colliding name gets a UUID fragment appended, making it unique." + (let ((first (cj/music-radio--disambiguate-name "Jazz Radio" "aaaabbbbcccc" '()))) + (should (equal first "Jazz_Radio")) + (should-not (equal (cj/music-radio--disambiguate-name "Jazz Radio" "aaaabbbbcccc" + (list first)) + first)))) + +;;; --------------------------- search-url ------------------------------------- + +(ert-deftest test-music-radio-search-url-encodes-query-and-limit () + "Normal: the search URL hex-encodes the query and carries the limit." + (let* ((cj/music-radio-search-limit 30) + (u (cj/music-radio--search-url "de1.api.radio-browser.info" "smooth jazz"))) + (should (string-match-p "smooth%20jazz" u)) + (should (string-match-p "limit=30" u)) + (should (string-match-p "/json/stations/search" u)))) + +(provide 'test-music-config--radio) +;;; test-music-config--radio.el ends here |
