55 tests covering struct operations, status updates, queue construction, parameter defaults, fetch behavior with mocked url-retrieve, error handling, timeout, parser modes, concurrency limits, result ordering and full integration flow.
887 lines
39 KiB
EmacsLisp
887 lines
39 KiB
EmacsLisp
;;; async-http-queue-test.el --- Tests for async-http-queue -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2025 Andros Fenollosa
|
|
|
|
;; Author: Andros Fenollosa <hi@andros.dev>
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Test suite for async-http-queue. Uses ERT with mocked `url-retrieve'
|
|
;; to test all functionality without network dependencies.
|
|
;;
|
|
;; To run all tests from the shell:
|
|
;; Emacs -Q --batch -L . -l async-http-queue-test.el -f ert-run-tests-batch-and-exit
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
(require 'cl-lib)
|
|
(require 'async-http-queue)
|
|
|
|
;;;; Test helpers
|
|
|
|
(defvar async-http-queue-test--timeout 5
|
|
"Maximum seconds to wait for async operations in tests.")
|
|
|
|
(defun async-http-queue-test--wait (predicate &optional timeout)
|
|
"Wait until PREDICATE return non-nil or TIMEOUT seconds elapse.
|
|
TIMEOUT defaults to `async-http-queue-test--timeout'."
|
|
(let ((deadline (+ (float-time) (or timeout async-http-queue-test--timeout))))
|
|
(while (and (not (funcall predicate))
|
|
(< (float-time) deadline))
|
|
(accept-process-output nil 0.05))
|
|
(funcall predicate)))
|
|
|
|
(defun async-http-queue-test--make-http-response (status-code body &optional content-type)
|
|
"Build a fake HTTP response string with STATUS-CODE and BODY.
|
|
CONTENT-TYPE defaults to application/json."
|
|
(let ((ct (or content-type "application/json")))
|
|
(format "HTTP/1.1 %d OK\r\nContent-Type: %s\r\n\r\n%s"
|
|
status-code ct body)))
|
|
|
|
(defun async-http-queue-test--mock-url-retrieve (responses)
|
|
"Return a function that replace `url-retrieve'.
|
|
RESPONSES is an alist of (URL . RESPONSE-STRING) or (URL . :timeout)
|
|
or (URL . :error). When a URL is fetched, the corresponding response
|
|
is delivered asynchronously via a timer."
|
|
(lambda (url callback &optional _cbargs _silent)
|
|
(let* ((entry (assoc url responses))
|
|
(response (cdr entry))
|
|
(buf (generate-new-buffer (format " *test-url-%s*" url))))
|
|
(with-current-buffer buf
|
|
(cond
|
|
((eq response :timeout)
|
|
;; Do not call the callback, simulating a hang
|
|
nil)
|
|
((eq response :error)
|
|
;; Call callback with error status after a short delay
|
|
(run-at-time 0.01 nil
|
|
(lambda ()
|
|
(with-current-buffer buf
|
|
(funcall callback (list :error '(error connection-refused)))))))
|
|
(t
|
|
;; Insert the response and call the callback after a short delay
|
|
(insert (or response
|
|
(async-http-queue-test--make-http-response 200 "{}")))
|
|
(run-at-time 0.01 nil
|
|
(lambda ()
|
|
(with-current-buffer buf
|
|
(funcall callback nil)))))))
|
|
buf)))
|
|
|
|
(defmacro async-http-queue-test--with-mock (responses &rest body)
|
|
"Execute BODY with `url-retrieve' replaced by a mock using RESPONSES."
|
|
(declare (indent 1))
|
|
`(cl-letf (((symbol-function 'url-retrieve)
|
|
(async-http-queue-test--mock-url-retrieve ,responses)))
|
|
,@body))
|
|
|
|
(defun async-http-queue-test--make-state (&optional urls)
|
|
"Create a test state with URLS (defaults to 3 test URLs)."
|
|
(let ((url-list (or urls '("http://test/1" "http://test/2" "http://test/3"))))
|
|
(async-http-queue--state-create
|
|
:queue (mapcar (lambda (url)
|
|
`((url . ,url)
|
|
(status . pending)
|
|
(data . nil)))
|
|
url-list)
|
|
:active-workers 0
|
|
:max-concurrent 5
|
|
:timeout 10
|
|
:parser #'json-parse-buffer
|
|
:completion-callback nil
|
|
:error-callback nil)))
|
|
|
|
;;;; Struct tests
|
|
|
|
(ert-deftest async-http-queue-test-state-create ()
|
|
"Verify struct creation with all fields."
|
|
(let ((state (async-http-queue--state-create
|
|
:queue '(((url . "http://a") (status . pending) (data . nil)))
|
|
:active-workers 0
|
|
:max-concurrent 3
|
|
:timeout 15
|
|
:parser #'json-parse-buffer
|
|
:completion-callback #'ignore
|
|
:error-callback #'ignore)))
|
|
(should (= (async-http-queue--state-active-workers state) 0))
|
|
(should (= (async-http-queue--state-max-concurrent state) 3))
|
|
(should (= (async-http-queue--state-timeout state) 15))
|
|
(should (eq (async-http-queue--state-parser state) #'json-parse-buffer))
|
|
(should (eq (async-http-queue--state-completion-callback state) #'ignore))
|
|
(should (eq (async-http-queue--state-error-callback state) #'ignore))
|
|
(should (= (length (async-http-queue--state-queue state)) 1))))
|
|
|
|
(ert-deftest async-http-queue-test-state-no-copier ()
|
|
"Verify copier is suppressed."
|
|
(should-not (fboundp 'copy-async-http-queue--state)))
|
|
|
|
;;;; Update status tests
|
|
|
|
(ert-deftest async-http-queue-test-update-status ()
|
|
"Verify status update for a matching URL."
|
|
(let ((state (async-http-queue-test--make-state)))
|
|
(async-http-queue--update-status state "http://test/2" 'processing)
|
|
(let ((item (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/2"))
|
|
(async-http-queue--state-queue state))))
|
|
(should (eq (alist-get 'status item) 'processing)))))
|
|
|
|
(ert-deftest async-http-queue-test-update-status-no-side-effects ()
|
|
"Verify status update does not affect other items."
|
|
(let ((state (async-http-queue-test--make-state)))
|
|
(async-http-queue--update-status state "http://test/2" 'processing)
|
|
(let ((item1 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/1"))
|
|
(async-http-queue--state-queue state)))
|
|
(item3 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/3"))
|
|
(async-http-queue--state-queue state))))
|
|
(should (eq (alist-get 'status item1) 'pending))
|
|
(should (eq (alist-get 'status item3) 'pending)))))
|
|
|
|
(ert-deftest async-http-queue-test-update-status-nonexistent-url ()
|
|
"Verify update with non-matching URL does not change anything."
|
|
(let* ((state (async-http-queue-test--make-state))
|
|
(queue-before (copy-tree (async-http-queue--state-queue state))))
|
|
(async-http-queue--update-status state "http://nonexistent" 'done)
|
|
(should (equal (mapcar (lambda (i) (alist-get 'status i))
|
|
(async-http-queue--state-queue state))
|
|
(mapcar (lambda (i) (alist-get 'status i))
|
|
queue-before)))))
|
|
|
|
;;;; Update data tests
|
|
|
|
(ert-deftest async-http-queue-test-update-data ()
|
|
"Verify data update for a matching URL."
|
|
(let ((state (async-http-queue-test--make-state))
|
|
(test-data '((title . "Hello"))))
|
|
(async-http-queue--update-data state "http://test/1" test-data)
|
|
(let ((item (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/1"))
|
|
(async-http-queue--state-queue state))))
|
|
(should (equal (alist-get 'data item) test-data)))))
|
|
|
|
(ert-deftest async-http-queue-test-update-data-no-side-effects ()
|
|
"Verify data update does not affect other items."
|
|
(let ((state (async-http-queue-test--make-state)))
|
|
(async-http-queue--update-data state "http://test/1" "some-data")
|
|
(let ((item2 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/2"))
|
|
(async-http-queue--state-queue state))))
|
|
(should (null (alist-get 'data item2))))))
|
|
|
|
;;;; Check completion tests
|
|
|
|
(ert-deftest async-http-queue-test-check-completion-all-done ()
|
|
"Verify completion fires when all items are done."
|
|
(let* ((result nil)
|
|
(state (async-http-queue-test--make-state '("http://a"))))
|
|
(setf (async-http-queue--state-completion-callback state)
|
|
(lambda (r) (setq result r)))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--update-data state "http://a" '((key . "val")))
|
|
(async-http-queue--check-completion state)
|
|
(should (vectorp result))
|
|
(should (= (length result) 1))
|
|
(should (equal (aref result 0) '((key . "val"))))))
|
|
|
|
(ert-deftest async-http-queue-test-check-completion-not-ready ()
|
|
"Verify completion does not fire while items are pending."
|
|
(let* ((called nil)
|
|
(state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
(setf (async-http-queue--state-completion-callback state)
|
|
(lambda (_) (setq called t)))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
;; http://b is still pending
|
|
(async-http-queue--check-completion state)
|
|
(should-not called)))
|
|
|
|
(ert-deftest async-http-queue-test-check-completion-with-errors ()
|
|
"Verify completion fires with nil for failed items."
|
|
(let* ((result nil)
|
|
(state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
(setf (async-http-queue--state-completion-callback state)
|
|
(lambda (r) (setq result r)))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--update-data state "http://a" "ok")
|
|
(async-http-queue--update-status state "http://b" 'error)
|
|
(async-http-queue--check-completion state)
|
|
(should (vectorp result))
|
|
(should (equal (aref result 0) "ok"))
|
|
(should (null (aref result 1)))))
|
|
|
|
(ert-deftest async-http-queue-test-check-completion-nil-callback ()
|
|
"Verify no error when completion-callback is nil."
|
|
(let ((state (async-http-queue-test--make-state '("http://a"))))
|
|
(setf (async-http-queue--state-completion-callback state) nil)
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--check-completion state)))
|
|
|
|
(ert-deftest async-http-queue-test-result-order-preserved ()
|
|
"Verify results vector preserves original URL order."
|
|
(let* ((result nil)
|
|
(urls '("http://a" "http://b" "http://c"))
|
|
(state (async-http-queue-test--make-state urls)))
|
|
(setf (async-http-queue--state-completion-callback state)
|
|
(lambda (r) (setq result r)))
|
|
;; Complete in reverse order
|
|
(async-http-queue--update-status state "http://c" 'done)
|
|
(async-http-queue--update-data state "http://c" "third")
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--update-data state "http://a" "first")
|
|
(async-http-queue--update-status state "http://b" 'done)
|
|
(async-http-queue--update-data state "http://b" "second")
|
|
(async-http-queue--check-completion state)
|
|
(should (equal (aref result 0) "first"))
|
|
(should (equal (aref result 1) "second"))
|
|
(should (equal (aref result 2) "third"))))
|
|
|
|
;;;; Empty URL list tests
|
|
|
|
(ert-deftest async-http-queue-test-empty-urls-with-callback ()
|
|
"Verify empty URL list calls callback with empty vector."
|
|
(let ((result nil))
|
|
(async-http-queue nil :callback (lambda (r) (setq result r)))
|
|
(should (vectorp result))
|
|
(should (= (length result) 0))))
|
|
|
|
(ert-deftest async-http-queue-test-empty-urls-no-callback ()
|
|
"Verify empty URL list with no callback does not error."
|
|
(async-http-queue nil))
|
|
|
|
;;;; Message tests
|
|
|
|
(ert-deftest async-http-queue-test-message-singular ()
|
|
"Verify singular message for 1 URL."
|
|
(let ((messages nil))
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages)))
|
|
((symbol-function 'url-retrieve)
|
|
(lambda (&rest _) (generate-new-buffer " *test*"))))
|
|
(async-http-queue '("http://test/1")
|
|
:callback #'ignore)
|
|
(should (member "Fetching 1 URL..." messages)))))
|
|
|
|
(ert-deftest async-http-queue-test-message-plural ()
|
|
"Verify plural message for multiple URLs."
|
|
(let ((messages nil))
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages)))
|
|
((symbol-function 'url-retrieve)
|
|
(lambda (&rest _) (generate-new-buffer " *test*"))))
|
|
(async-http-queue '("http://test/1" "http://test/2")
|
|
:callback #'ignore)
|
|
(should (member "Fetching 2 URLs..." messages)))))
|
|
|
|
(ert-deftest async-http-queue-test-final-message-no-failures ()
|
|
"Verify final message when all succeed."
|
|
(let* ((messages nil)
|
|
(state (async-http-queue-test--make-state '("http://a"))))
|
|
(setf (async-http-queue--state-completion-callback state) #'ignore)
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages))))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--check-completion state)
|
|
(should (member "Loaded 1 URLs" messages)))))
|
|
|
|
(ert-deftest async-http-queue-test-final-message-with-failures ()
|
|
"Verify final message includes failure count."
|
|
(let* ((messages nil)
|
|
(state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
(setf (async-http-queue--state-completion-callback state) #'ignore)
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages))))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--update-status state "http://b" 'error)
|
|
(async-http-queue--check-completion state)
|
|
(should (member "Loaded 1 URLs (1 failed)" messages)))))
|
|
|
|
(ert-deftest async-http-queue-test-progress-message-large-batch ()
|
|
"Verify progress messages appear for batches > 10."
|
|
(let* ((messages nil)
|
|
(urls (cl-loop for i from 1 to 12
|
|
collect (format "http://test/%d" i)))
|
|
(state (async-http-queue-test--make-state urls)))
|
|
(setf (async-http-queue--state-completion-callback state) #'ignore)
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages))))
|
|
;; Complete only some items so in-progress > 0
|
|
(async-http-queue--update-status state "http://test/1" 'done)
|
|
(async-http-queue--check-completion state)
|
|
;; Should have a progress message
|
|
(should (cl-some (lambda (m) (string-match "Loading URLs\\.\\.\\." m))
|
|
messages)))))
|
|
|
|
(ert-deftest async-http-queue-test-no-progress-message-small-batch ()
|
|
"Verify no progress messages for batches <= 10."
|
|
(let* ((messages nil)
|
|
(state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
(cl-letf (((symbol-function 'message)
|
|
(lambda (fmt &rest args)
|
|
(push (apply #'format fmt args) messages))))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(async-http-queue--check-completion state)
|
|
(should-not (cl-some (lambda (m) (string-match "Loading URLs" m))
|
|
messages)))))
|
|
|
|
;;;; Default parameter tests
|
|
|
|
(ert-deftest async-http-queue-test-default-max-concurrent ()
|
|
"Verify default max-concurrent is 5."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1"))
|
|
(should (= (async-http-queue--state-max-concurrent captured-state) 5)))))
|
|
|
|
(ert-deftest async-http-queue-test-default-timeout ()
|
|
"Verify default timeout is 10."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1"))
|
|
(should (= (async-http-queue--state-timeout captured-state) 10)))))
|
|
|
|
(ert-deftest async-http-queue-test-default-parser ()
|
|
"Verify default parser is `json-parse-buffer'."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1"))
|
|
(should (eq (async-http-queue--state-parser captured-state)
|
|
#'json-parse-buffer)))))
|
|
|
|
(ert-deftest async-http-queue-test-custom-max-concurrent ()
|
|
"Verify custom max-concurrent is stored."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1") :max-concurrent 3)
|
|
(should (= (async-http-queue--state-max-concurrent captured-state) 3)))))
|
|
|
|
(ert-deftest async-http-queue-test-custom-timeout ()
|
|
"Verify custom timeout is stored."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1") :timeout 30)
|
|
(should (= (async-http-queue--state-timeout captured-state) 30)))))
|
|
|
|
(ert-deftest async-http-queue-test-nil-parser ()
|
|
"Verify nil parser is stored for raw text mode."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1") :parser nil)
|
|
(should (null (async-http-queue--state-parser captured-state))))))
|
|
|
|
(ert-deftest async-http-queue-test-custom-parser ()
|
|
"Verify custom parser function is stored."
|
|
(let (captured-state
|
|
(my-parser (lambda () "custom")))
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://test/1") :parser my-parser)
|
|
(should (eq (async-http-queue--state-parser captured-state)
|
|
my-parser)))))
|
|
|
|
;;;; Queue construction tests
|
|
|
|
(ert-deftest async-http-queue-test-queue-construction ()
|
|
"Verify queue items have correct shape."
|
|
(let (captured-state)
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://a" "http://b"))
|
|
(let ((queue (async-http-queue--state-queue captured-state)))
|
|
(should (= (length queue) 2))
|
|
(dolist (item queue)
|
|
(should (assoc 'url item))
|
|
(should (assoc 'status item))
|
|
(should (assoc 'data item))
|
|
(should (eq (alist-get 'status item) 'pending))
|
|
(should (null (alist-get 'data item))))
|
|
(should (equal (alist-get 'url (nth 0 queue)) "http://a"))
|
|
(should (equal (alist-get 'url (nth 1 queue)) "http://b"))))))
|
|
|
|
(ert-deftest async-http-queue-test-callbacks-stored ()
|
|
"Verify callbacks are stored in state."
|
|
(let (captured-state
|
|
(my-cb (lambda (_) nil))
|
|
(my-err-cb (lambda (_) nil)))
|
|
(cl-letf (((symbol-function 'async-http-queue--process)
|
|
(lambda (state) (setq captured-state state)))
|
|
((symbol-function 'message) #'ignore))
|
|
(async-http-queue '("http://a")
|
|
:callback my-cb
|
|
:error-callback my-err-cb)
|
|
(should (eq (async-http-queue--state-completion-callback captured-state) my-cb))
|
|
(should (eq (async-http-queue--state-error-callback captured-state) my-err-cb)))))
|
|
|
|
;;;; Process-next-pending tests
|
|
|
|
(ert-deftest async-http-queue-test-process-next-respects-limit ()
|
|
"Verify no new worker starts when at capacity."
|
|
(let* ((fetch-called nil)
|
|
(state (async-http-queue-test--make-state '("http://a"))))
|
|
(setf (async-http-queue--state-active-workers state) 5)
|
|
(setf (async-http-queue--state-max-concurrent state) 5)
|
|
(cl-letf (((symbol-function 'async-http-queue--fetch-url)
|
|
(lambda (&rest _) (setq fetch-called t))))
|
|
(async-http-queue--process-next-pending state)
|
|
(should-not fetch-called))))
|
|
|
|
(ert-deftest async-http-queue-test-process-next-starts-worker ()
|
|
"Verify a worker starts when below capacity."
|
|
(let* ((fetch-called nil)
|
|
(fetched-url nil)
|
|
(state (async-http-queue-test--make-state '("http://a"))))
|
|
(setf (async-http-queue--state-active-workers state) 0)
|
|
(setf (async-http-queue--state-max-concurrent state) 5)
|
|
(cl-letf (((symbol-function 'async-http-queue--fetch-url)
|
|
(lambda (_state url _success _error)
|
|
(setq fetch-called t
|
|
fetched-url url))))
|
|
(async-http-queue--process-next-pending state)
|
|
(should fetch-called)
|
|
(should (equal fetched-url "http://a"))
|
|
(should (= (async-http-queue--state-active-workers state) 1)))))
|
|
|
|
(ert-deftest async-http-queue-test-process-next-skips-non-pending ()
|
|
"Verify already-processing items are skipped."
|
|
(let* ((fetch-called nil)
|
|
(state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
(async-http-queue--update-status state "http://a" 'processing)
|
|
(setf (async-http-queue--state-active-workers state) 1)
|
|
(cl-letf (((symbol-function 'async-http-queue--fetch-url)
|
|
(lambda (_state url _success _error)
|
|
(setq fetch-called url))))
|
|
(async-http-queue--process-next-pending state)
|
|
(should (equal fetch-called "http://b")))))
|
|
|
|
(ert-deftest async-http-queue-test-process-next-noop-no-pending ()
|
|
"Verify noop when no pending items remain."
|
|
(let* ((fetch-called nil)
|
|
(state (async-http-queue-test--make-state '("http://a"))))
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(setf (async-http-queue--state-active-workers state) 0)
|
|
(cl-letf (((symbol-function 'async-http-queue--fetch-url)
|
|
(lambda (&rest _) (setq fetch-called t))))
|
|
(async-http-queue--process-next-pending state)
|
|
(should-not fetch-called))))
|
|
|
|
;;;; Status transition tests
|
|
|
|
(ert-deftest async-http-queue-test-status-transitions ()
|
|
"Verify the valid status transitions: pending -> processing -> done/error."
|
|
(let ((state (async-http-queue-test--make-state '("http://a" "http://b"))))
|
|
;; Start as pending
|
|
(should (eq (alist-get 'status
|
|
(nth 0 (async-http-queue--state-queue state)))
|
|
'pending))
|
|
;; Transition to processing
|
|
(async-http-queue--update-status state "http://a" 'processing)
|
|
(should (eq (alist-get 'status
|
|
(seq-find (lambda (i) (equal (alist-get 'url i) "http://a"))
|
|
(async-http-queue--state-queue state)))
|
|
'processing))
|
|
;; Transition to done
|
|
(async-http-queue--update-status state "http://a" 'done)
|
|
(should (eq (alist-get 'status
|
|
(seq-find (lambda (i) (equal (alist-get 'url i) "http://a"))
|
|
(async-http-queue--state-queue state)))
|
|
'done))
|
|
;; Transition to error
|
|
(async-http-queue--update-status state "http://b" 'processing)
|
|
(async-http-queue--update-status state "http://b" 'error)
|
|
(should (eq (alist-get 'status
|
|
(seq-find (lambda (i) (equal (alist-get 'url i) "http://b"))
|
|
(async-http-queue--state-queue state)))
|
|
'error))))
|
|
|
|
;;;; Fetch URL tests (with mocked url-retrieve)
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-success-json ()
|
|
"Verify successful fetch with JSON parser."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(json-body "{\"title\": \"hello\"}")
|
|
(response (async-http-queue-test--make-http-response 200 json-body))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser #'json-parse-buffer
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should-not error-called)
|
|
(should result)
|
|
(should (equal (gethash "title" result) "hello")))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-success-raw-text ()
|
|
"Verify successful fetch with nil parser returns raw text."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(body "Hello, world!")
|
|
(response (async-http-queue-test--make-http-response 200 body "text/plain"))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should-not error-called)
|
|
(should (equal result "Hello, world!")))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-success-custom-parser ()
|
|
"Verify successful fetch with custom parser."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(body "some data")
|
|
(response (async-http-queue-test--make-http-response 200 body "text/plain"))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10
|
|
:parser (lambda ()
|
|
(upcase (buffer-substring-no-properties
|
|
(point) (point-max))))
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should-not error-called)
|
|
(should (equal result "SOME DATA")))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-http-error ()
|
|
"Verify non-2xx status code triggers error callback."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(response (async-http-queue-test--make-http-response 404 "Not Found"))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-500-error ()
|
|
"Verify 500 status triggers error callback."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(response (async-http-queue-test--make-http-response 500 "Internal Server Error"))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-connection-error ()
|
|
"Verify connection error triggers error callback."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . :error))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-parser-error ()
|
|
"Verify parser failure triggers error callback."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(response (async-http-queue-test--make-http-response 200 "not valid json"))
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser #'json-parse-buffer
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-invalid-response ()
|
|
"Verify invalid HTTP response (no status line) triggers error."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 10 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . "garbage response with no http status"))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () (or result error-called))))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
(ert-deftest async-http-queue-test-fetch-url-timeout ()
|
|
"Verify timeout triggers error callback."
|
|
(let* ((result nil)
|
|
(error-called nil)
|
|
(state (async-http-queue--state-create
|
|
:queue nil :active-workers 0 :max-concurrent 5
|
|
:timeout 0.1 :parser nil
|
|
:completion-callback nil :error-callback nil)))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . :timeout))
|
|
(async-http-queue--fetch-url
|
|
state "http://test/1"
|
|
(lambda (data) (setq result data))
|
|
(lambda () (setq error-called t)))
|
|
(should (async-http-queue-test--wait (lambda () error-called) 3))
|
|
(should error-called)
|
|
(should-not result))))
|
|
|
|
;;;; Full integration tests (with mocked url-retrieve)
|
|
|
|
(ert-deftest async-http-queue-test-integration-single-url ()
|
|
"Verify full flow with a single URL."
|
|
(let* ((result nil)
|
|
(json-body "{\"id\": 1}")
|
|
(response (async-http-queue-test--make-http-response 200 json-body)))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue '("http://test/1")
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (vectorp result))
|
|
(should (= (length result) 1))
|
|
(should (= (gethash "id" (aref result 0)) 1)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-multiple-urls ()
|
|
"Verify full flow with multiple URLs."
|
|
(let* ((result nil)
|
|
(responses `(("http://test/1"
|
|
. ,(async-http-queue-test--make-http-response
|
|
200 "{\"id\": 1}"))
|
|
("http://test/2"
|
|
. ,(async-http-queue-test--make-http-response
|
|
200 "{\"id\": 2}"))
|
|
("http://test/3"
|
|
. ,(async-http-queue-test--make-http-response
|
|
200 "{\"id\": 3}")))))
|
|
(async-http-queue-test--with-mock responses
|
|
(async-http-queue '("http://test/1" "http://test/2" "http://test/3")
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (= (length result) 3))
|
|
(should (= (gethash "id" (aref result 0)) 1))
|
|
(should (= (gethash "id" (aref result 1)) 2))
|
|
(should (= (gethash "id" (aref result 2)) 3)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-mixed-success-failure ()
|
|
"Verify results contain nil for failed URLs."
|
|
(let* ((result nil)
|
|
(error-urls nil)
|
|
(responses `(("http://test/1"
|
|
. ,(async-http-queue-test--make-http-response
|
|
200 "{\"id\": 1}"))
|
|
("http://test/2"
|
|
. ,(async-http-queue-test--make-http-response
|
|
500 "error"))
|
|
("http://test/3"
|
|
. ,(async-http-queue-test--make-http-response
|
|
200 "{\"id\": 3}")))))
|
|
(async-http-queue-test--with-mock responses
|
|
(async-http-queue '("http://test/1" "http://test/2" "http://test/3")
|
|
:callback (lambda (r) (setq result r))
|
|
:error-callback (lambda (url) (push url error-urls)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (= (length result) 3))
|
|
(should (= (gethash "id" (aref result 0)) 1))
|
|
(should (null (aref result 1)))
|
|
(should (= (gethash "id" (aref result 2)) 3))
|
|
(should (member "http://test/2" error-urls)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-all-fail ()
|
|
"Verify all-failure scenario."
|
|
(let* ((result nil)
|
|
(error-urls nil)
|
|
(responses '(("http://test/1" . :error)
|
|
("http://test/2" . :error))))
|
|
(async-http-queue-test--with-mock responses
|
|
(async-http-queue '("http://test/1" "http://test/2")
|
|
:callback (lambda (r) (setq result r))
|
|
:error-callback (lambda (url) (push url error-urls)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (= (length result) 2))
|
|
(should (null (aref result 0)))
|
|
(should (null (aref result 1)))
|
|
(should (= (length error-urls) 2)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-raw-text ()
|
|
"Verify raw text mode with nil parser."
|
|
(let* ((result nil)
|
|
(body "Hello raw text")
|
|
(response (async-http-queue-test--make-http-response
|
|
200 body "text/plain")))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue '("http://test/1")
|
|
:parser nil
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (equal (aref result 0) "Hello raw text")))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-custom-parser ()
|
|
"Verify custom parser in full flow."
|
|
(let* ((result nil)
|
|
(body "<root>data</root>")
|
|
(response (async-http-queue-test--make-http-response
|
|
200 body "text/xml")))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1" . ,response))
|
|
(async-http-queue '("http://test/1")
|
|
:parser (lambda ()
|
|
(upcase (buffer-substring-no-properties
|
|
(point) (point-max))))
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (equal (aref result 0) "<ROOT>DATA</ROOT>")))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-custom-concurrency ()
|
|
"Verify custom concurrency is passed through."
|
|
(let* ((result nil)
|
|
(max-seen-workers 0)
|
|
(response (async-http-queue-test--make-http-response 200 "\"ok\"")))
|
|
(cl-letf (((symbol-function 'url-retrieve)
|
|
(lambda (_url callback &optional _cbargs _silent)
|
|
(let ((buf (generate-new-buffer " *test*")))
|
|
(with-current-buffer buf
|
|
(insert response))
|
|
;; Delay to allow concurrency measurement
|
|
(run-at-time 0.05 nil
|
|
(lambda ()
|
|
(with-current-buffer buf
|
|
(funcall callback nil))))
|
|
buf)))
|
|
((symbol-function 'async-http-queue--process-next-pending)
|
|
(let ((orig (symbol-function 'async-http-queue--process-next-pending)))
|
|
(lambda (state)
|
|
(funcall orig state)
|
|
(setq max-seen-workers
|
|
(max max-seen-workers
|
|
(async-http-queue--state-active-workers state)))))))
|
|
(async-http-queue '("http://test/1" "http://test/2" "http://test/3"
|
|
"http://test/4" "http://test/5")
|
|
:max-concurrent 2
|
|
:parser nil
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (<= max-seen-workers 2)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-error-callback-called ()
|
|
"Verify error-callback receives the failed URL."
|
|
(let* ((result nil)
|
|
(failed-url nil))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . :error))
|
|
(async-http-queue '("http://test/1")
|
|
:callback (lambda (r) (setq result r))
|
|
:error-callback (lambda (url) (setq failed-url url)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (equal failed-url "http://test/1")))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-no-error-callback ()
|
|
"Verify no crash when error-callback is nil and errors occur."
|
|
(let ((result nil))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . :error))
|
|
(async-http-queue '("http://test/1")
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (null (aref result 0))))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-timeout ()
|
|
"Verify timeout in full flow."
|
|
(let* ((result nil)
|
|
(error-urls nil))
|
|
(async-http-queue-test--with-mock
|
|
'(("http://test/1" . :timeout))
|
|
(async-http-queue '("http://test/1")
|
|
:timeout 0.2
|
|
:callback (lambda (r) (setq result r))
|
|
:error-callback (lambda (url) (push url error-urls)))
|
|
(should (async-http-queue-test--wait (lambda () result) 5))
|
|
(should (null (aref result 0)))
|
|
(should (member "http://test/1" error-urls)))))
|
|
|
|
(ert-deftest async-http-queue-test-integration-result-is-vector ()
|
|
"Verify callback receives a vector, not a list."
|
|
(let ((result nil))
|
|
(async-http-queue-test--with-mock
|
|
`(("http://test/1"
|
|
. ,(async-http-queue-test--make-http-response 200 "\"data\"")))
|
|
(async-http-queue '("http://test/1")
|
|
:callback (lambda (r) (setq result r)))
|
|
(should (async-http-queue-test--wait (lambda () result)))
|
|
(should (vectorp result)))))
|
|
|
|
;;;; Provide test feature
|
|
|
|
(ert-deftest async-http-queue-test-provide ()
|
|
"Verify the package provides the expected feature."
|
|
(should (featurep 'async-http-queue)))
|
|
|
|
(provide 'async-http-queue-test)
|
|
|
|
;;; async-http-queue-test.el ends here
|