From 8f7f44aeca735a988c9eb0a18aca3497f07c8480 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Fri, 17 Nov 2023 06:58:44 -0800 Subject: [PATCH 0/3] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (3): [5.6] Make wrangling ISUPPORT data more convenient in ERC [5.6] Use caching variant of erc-parse-prefix internally [5.6] Rework MODE processing in ERC etc/ERC-NEWS | 11 + lisp/erc/erc-backend.el | 27 +- lisp/erc/erc-common.el | 16 + lisp/erc/erc.el | 279 ++++++++++++++++-- .../lisp/erc/erc-scenarios-base-chan-modes.el | 84 ++++++ .../lisp/erc/erc-scenarios-display-message.el | 2 - test/lisp/erc/erc-tests.el | 198 +++++++++++++ .../erc/resources/base/modes/chan-changed.eld | 55 ++++ 8 files changed, 636 insertions(+), 36 deletions(-) create mode 100644 test/lisp/erc/erc-scenarios-base-chan-modes.el create mode 100644 test/lisp/erc/resources/base/modes/chan-changed.eld Interdiff: diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el index ace46cf84f5..7b5d1e35189 100644 --- a/lisp/erc/erc-backend.el +++ b/lisp/erc/erc-backend.el @@ -2107,6 +2107,18 @@ erc--get-isupport-entry (when table (remhash key table)))) +(defmacro erc--with-isupport-data (param var &rest body) + "Return processed data for \"ISUPPORT\" PARAM value stored VAR. +Expect VAR's value to be an instance of an object whose \"class\" +inherits from `erc--isupport-data'. If VAR is uninitialized or +stale, evaluate BODY and assign the result to VAR." + (declare (indent defun)) + `(erc-with-server-buffer + (pcase-let (((,@(list '\` (list param '\, 'key))) + (erc--get-isupport-entry ',param))) + (or (and ,var (eq key (erc--isupport-data-key ,var)) ,var) + (setq ,var (progn ,@body)))))) + (define-erc-response-handler (005) "Set the variable `erc-server-parameters' and display the received message. diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index 930e8032f6d..48d29883d8f 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -101,6 +101,22 @@ erc--target (contents "" :type string) (tags '() :type list)) +(cl-defstruct erc--isupport-data + "Abstract class for parsed ISUPPORT data." + (key nil :type (or null cons))) + +(cl-defstruct (erc--parsed-prefix (:include erc--isupport-data)) + "Server-local data for recognized membership-status prefixes. +Derived from the advertised \"PREFIX\" ISUPPORT parameter." + (letters "qaohv" :type string) + (statuses "~&@%+" :type string) + (alist nil :type (list-of cons))) + +(cl-defstruct (erc--channel-mode-types (:include erc--isupport-data)) + "Server-local \"CHANMODES\" data." + (fallbackp nil :type boolean) + (table (make-char-table 'erc--channel-mode-types) :type char-table)) + ;; After dropping 28, we can use prefixed "erc-autoload" cookies. (defun erc--normalize-module-symbol (symbol) "Return preferred SYMBOL for `erc--modules'." diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 8a74414cb0c..78a4f363af2 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -5921,10 +5921,10 @@ erc-set-initial-user-mode (let* ((mode (if (functionp erc-user-mode) (funcall erc-user-mode) erc-user-mode)) - (as-pair (erc--parse-user-modes mode)) - (have (erc--user-modes)) - (redundant-want (seq-intersection (car as-pair) have)) - (redundant-drop (seq-difference (cadr as-pair) have))) + (groups (erc--parse-user-modes mode (erc--user-modes) t)) + (superfluous (last groups 2)) + (redundant-want (car superfluous)) + (redundant-drop (cadr superfluous))) (when redundant-want (erc-display-message nil 'notice buffer 'user-mode-redundant-add ?m (apply #'string redundant-want))) @@ -6221,38 +6221,21 @@ erc-parse-prefix collected)) (defvar-local erc--parsed-prefix nil - "Cons of latest advertised PREFIX and its parsed alist. -Only usable for the current server session.") - -;; As of ERC 5.6, `erc-channel-receive-names' is the only caller, and -;; it runs infrequently. In the future, extensions, like -;; `multi-prefix', may benefit more from a two-way translation table. -(cl-defstruct erc--parsed-prefix - "Server-local channel-membership-prefix data." - (key nil :type (or null string)) - (letters "qaohv" :type string) - (statuses "~&@%+" :type string) - (alist nil :type (list-of cons))) - -(defun erc--parse-prefix () - "Return (possibly cached) status prefix translation alist for the server. + "Current `erc--parsed-prefix' struct instance for the server.") + +(defun erc--parsed-prefix () + "Return possibly cached `erc--parsed-prefix' object for the server. Ensure the returned value describes the most recent \"PREFIX\" -ISUPPORT parameter received from the current server and that the -original ordering is preserved." - (erc-with-server-buffer - (let ((key (erc--get-isupport-entry 'PREFIX))) - (or (and key - erc--parsed-prefix - (eq (cdr key) (erc--parsed-prefix-key erc--parsed-prefix)) - (erc--parsed-prefix-alist erc--parsed-prefix)) - (let ((alist (nreverse (erc-parse-prefix)))) - (setq erc--parsed-prefix - (make-erc--parsed-prefix - :key (cdr key) - :letters (apply #'string (map-keys alist)) - :statuses (apply #'string (map-values alist)) - :alist alist)) - alist))))) +ISUPPORT parameter received from the current server, with the +original ordering intact. If no such parameter has yet arrived, +return a stand-in from the standard value \"(qaohv)~&@%+\"." + (erc--with-isupport-data PREFIX erc--parsed-prefix + (let ((alist (nreverse (erc-parse-prefix)))) + (make-erc--parsed-prefix + :key key + :letters (apply #'string (map-keys alist)) + :statuses (apply #'string (map-values alist)) + :alist alist)))) (defcustom erc-channel-members-changed-hook nil "This hook is called every time the variable `channel-members' changes. @@ -6266,7 +6249,7 @@ erc-channel-receive-names Update `erc-channel-users' according to NAMES-STRING. NAMES-STRING is a string listing some of the names on the channel." - (let* ((prefix (erc-parse-prefix)) + (let* ((prefix (erc--parsed-prefix-alist (erc--parsed-prefix))) (voice-ch (cdr (assq ?v prefix))) (op-ch (cdr (assq ?o prefix))) (hop-ch (cdr (assq ?h prefix))) @@ -6657,115 +6640,175 @@ erc--update-membership-prefix (and (= letter ?a) state) (and (= letter ?q) state))) -(defvar erc--update-channel-modes-omit-status-p nil) - -(defun erc--update-channel-modes (string &rest args) - "Update `erc-channel-modes' and dispatch individual mode handlers. -Also update status prefixes, as needed. Expect STRING to be a -\"modestring\" and ARGS to match mode-specific parameters. When -`erc--update-channel-modes-omit-status-p' is non-nil, forgo -setting status prefixes for channel members." - (cl-assert erc-server-process) - (cl-assert erc--target) +(defvar-local erc--channel-modes nil + "When non-nil, a hash table of current channel modes. +Keys are characters. Values are either a string, for types A-C, +or t, for type D.") + +(defvar-local erc--channel-mode-types nil + "Current `erc--channel-mode-types' instance for the server.") + +(defun erc--channel-mode-types () + "Return `erc--channel-mode-types', possibly creating it." + (erc--with-isupport-data CHANMODES erc--channel-mode-types + (let ((types (or key '(nil "Kk" "Ll" nil))) + (ct (make-char-table 'erc--channel-mode-types)) + (type ?a)) + (dolist (cs types) + (seq-doseq (c cs) + (aset ct c type)) + (cl-incf type)) + (make-erc--channel-mode-types :key key + :fallbackp (null key) + :table ct)))) + +(defun erc--process-channel-modes (string args &optional status-letters) + "Parse channel \"MODE\" changes and call unary letter handlers. +Update `erc-channel-modes' and `erc--channel-modes'. With +STATUS-LETTERS, also update channel membership prefixes. Expect +STRING to be the second argument from an incoming \"MODE\" +command and ARGS to be the remaining arguments, which should +complement relevant letters in STRING." (cl-assert (erc--target-channel-p erc--target)) - (pcase-let* ((status-letters - (and (not erc--update-channel-modes-omit-status-p) - (or (erc-with-server-buffer - (erc--parse-prefix) - (erc--parsed-prefix-letters erc--parsed-prefix)) - "qaovhbQAOVHB"))) - (`(,type-a ,type-b ,type-c ,type-d) - (or (cdr (erc--get-isupport-entry 'CHANMODES)) - '(nil "Kk" "Ll" nil))) - (+p t)) + (let* ((obj (erc--channel-mode-types)) + (table (erc--channel-mode-types-table obj)) + (fallbackp (erc--channel-mode-types-fallbackp obj)) + (+p t)) (dolist (c (append string nil)) (let ((letter (char-to-string c))) (cond ((= ?+ c) (setq +p t)) ((= ?- c) (setq +p nil)) ((and status-letters (string-search letter status-letters)) (erc--update-membership-prefix (pop args) c (if +p 'on 'off))) - ((and type-a (string-search letter type-a)) - (erc--handle-channel-mode 'a c +p (pop args))) - ((string-search letter type-b) - (erc--handle-channel-mode 'b c +p (pop args))) - ((string-search letter type-c) - (erc--handle-channel-mode 'c c +p (and +p (pop args)))) - ((or (null type-d) (string-search letter type-d)) - (setq erc-channel-modes - (if +p - (cl-pushnew letter erc-channel-modes :test #'equal) - (delete letter erc-channel-modes)))) - (type-d ; OK to print error because server buffer exists + ((and-let* ((group (or (aref table c) (and fallbackp ?d)))) + (erc--handle-channel-mode group c +p + (and (or (/= group ?c) +p) + (pop args))) + t)) + ((not fallbackp) (erc-display-message nil '(notice error) (erc-server-buffer) (format "Unknown channel mode: %S" c)))))) - (setq erc-channel-modes (erc-sort-strings erc-channel-modes)) + (setq erc-channel-modes (sort erc-channel-modes #'string<)) (erc-update-mode-line (current-buffer)))) (defvar-local erc--user-modes nil - "List of current user modes, analogous to `erc-channel-modes'.") - -(defun erc--user-modes (&optional as-string-p) - "Return user mode letters as chars or, with AS-STRING-P, a single string." - (let ((modes (erc-with-server-buffer erc--user-modes))) - (if as-string-p - (apply #'string (if (memq as-string-p '(+ ?+)) (cons '?+ modes) modes)) - modes))) - -(defun erc--parse-user-modes (string) - "Return a list of mode chars to add and remove, based on STRING." + "Sorted list of current user \"MODE\" letters. +Analogous to `erc-channel-modes' but chars rather than strings.") + +(defun erc--user-modes (&optional as-type) + "Return user \"MODE\" letters in a form described by AS-TYPE. +When AS-TYPE is the symbol `strings' (plural), return a list of +strings. When it's `string' (singular), return the same list +concatenated into a single string. When it's a single char, like +?+, return the same value as `string' but with AS-TYPE prepended. +When AS-TYPE is nil, return a list of chars." + (let ((modes (or erc--user-modes (erc-with-server-buffer erc--user-modes)))) + (pcase as-type + ('strings (mapcar #'char-to-string modes)) + ('string (apply #'string modes)) + ((and (pred characterp) c) (apply #'string (cons c modes))) + (_ modes)))) + +(defun erc--parse-user-modes (string &optional current extrap) + "Return lists of chars from STRING to add to and drop from CURRENT. +Expect STRING to be a so-called \"modestring\", the second +parameter of a \"MODE\" command, here containing only valid +user-mode letters. Expect CURRENT to be a list of chars +resembling those found in `erc--user-modes'. With EXTRAP, return +two additional lists of chars: those that would be added were +they not already present in CURRENT and those that would be +dropped were they not already absent." (let ((addp t) - add-modes remove-modes) + ;; + redundant-add redundant-drop adding dropping) (seq-doseq (c string) (pcase c (?+ (setq addp t)) (?- (setq addp nil)) - (_ (push c (if addp add-modes remove-modes))))) - (list (nreverse add-modes) - (nreverse remove-modes)))) + (_ (push c (let ((hasp (and current (memq c current)))) + (if addp + (if hasp redundant-add adding) + (if hasp dropping redundant-drop))))))) + (if extrap + (list (nreverse adding) (nreverse dropping) + (nreverse redundant-add) (nreverse redundant-drop)) + (list (nreverse adding) (nreverse dropping))))) + +(defun erc--update-user-modes (string) + "Update `erc--user-modes' from \"MODE\" STRING. +Return a list of characters sorted by character code." + (setq erc--user-modes + (pcase-let ((`(,adding ,dropping) + (erc--parse-user-modes string erc--user-modes))) + (sort (seq-difference (nconc erc--user-modes adding) dropping) + #'<)))) -(defun erc--merge-user-modes (adding dropping) - "Update `erc--user-modes' with chars ADDING and DROPPING." - (sort (seq-difference (seq-union erc--user-modes adding) dropping) #'-)) +(defun erc--update-channel-modes (string &rest args) + "Update `erc-channel-modes' and call individual mode handlers. +Also update membership prefixes, as needed. Expect STRING to be +a \"modestring\" and ARGS to match mode-specific parameters." + (let ((status-letters (or (erc-with-server-buffer + (erc--parsed-prefix-letters + (erc--parsed-prefix))) + "qaovhbQAOVHB"))) + (erc--process-channel-modes string args status-letters))) ;; XXX this comment is referenced elsewhere (grep before deleting). ;; ;; The function `erc-update-modes' was deprecated in ERC 5.6 with no ;; immediate public replacement. Third parties needing such a thing ;; are encouraged to write to emacs-erc@gnu.org with ideas for a -;; mode-handler API, possibly one incorporating mode-letter specific -;; handlers, like `erc--handle-channel-mode' below. +;; mode-handler API, possibly one incorporating letter-specific +;; handlers, like `erc--handle-channel-mode' (below), which only +;; handles mode types A-C. (defun erc--update-modes (raw-args) - "Handle user or channel mode update from server. -Expect RAW-ARGS to be a \"modestring\" followed by mode-specific -arguments." + "Handle user or channel \"MODE\" update from server. +Expect RAW-ARGS be a list consisting of a \"modestring\" followed +by mode-specific arguments." (if (and erc--target (erc--target-channel-p erc--target)) (apply #'erc--update-channel-modes raw-args) - (setq erc--user-modes - (apply #'erc--merge-user-modes - (erc--parse-user-modes (car raw-args)))))) + (erc--update-user-modes (car raw-args)))) (defun erc--init-channel-modes (channel raw-args) - "Set CHANNEL modes from RAW-ARGS." - (let ((erc--update-channel-modes-omit-status-p t)) - (erc-with-buffer (channel) - (apply #'erc--update-channel-modes raw-args)))) + "Set CHANNEL modes from RAW-ARGS. +Expect RAW-ARGS to be a \"modestring\" without any status-prefix +chars, followed by applicable arguments." + (erc-with-buffer (channel) + (erc--process-channel-modes (car raw-args) (cdr raw-args)))) (cl-defgeneric erc--handle-channel-mode (type letter state arg) "Handle a STATE change for mode LETTER of TYPE with ARG. Expect to be called in the affected target buffer. Expect TYPE -to be a symbol, namely, one of `a', `b', `c', or `d'. Expect -LETTER to be a character, STATE to be a boolean, and ARGUMENT to -be either a string or nil." +to be a character, like ?a, representing an advertised +\"CHANMODES\" group. Expect LETTER to also be a character, and +expect STATE to be a boolean and ARGUMENT either a string or nil." (erc-log (format "Channel-mode %c (type %s, arg %S) %s" letter type arg (if state 'enabled 'disabled)))) -;; We could specialize on (eql 'c), but that may be too brittle. +(cl-defmethod erc--handle-channel-mode :before (_ c state arg) + "Record STATE change and ARG, if enabling, for mode letter C." + (unless erc--channel-modes + (cl-assert (erc--target-channel-p erc--target)) + (setq erc--channel-modes (make-hash-table))) + (if state + (puthash c (or arg t) erc--channel-modes) + (remhash c erc--channel-modes))) + +(cl-defmethod erc--handle-channel-mode :before ((_ (eql ?d)) c state _) + "Update `erc-channel-modes' for any character C of nullary type D. +Remember when STATE is non-nil and forget otherwise." + (setq erc-channel-modes + (if state + (cl-pushnew (char-to-string c) erc-channel-modes :test #'equal) + (delete (char-to-string c) erc-channel-modes)))) + +;; We could specialize on type C, but that may be too brittle. (cl-defmethod erc--handle-channel-mode (_ (_ (eql ?l)) state arg) (erc-update-channel-limit (erc--target-string erc--target) (if state 'on 'off) arg)) -;; We could specialize on (eql 'b), but that may be too brittle. +;; We could specialize on type B, but that may be too brittle. (cl-defmethod erc--handle-channel-mode (_ (_ (eql ?k)) state arg) ;; Mimic old parsing behavior in which an ARG of "*" was discarded ;; even though `erc-update-channel-limit' checks STATE first. diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 1ff5f4890a8..b7a0b29d06d 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -643,11 +643,24 @@ erc-parse-user (should (equal '("de" "" "fg@xy") (erc-parse-user "abc\nde!fg@xy")))))) -(ert-deftest erc--parse-prefix () +(ert-deftest erc--parsed-prefix () (erc-mode) (erc-tests--set-fake-server-process "sleep" "1") - (setq erc--isupport-params (make-hash-table) - erc-server-parameters '(("PREFIX" . "(Yqaohv)!~&@%+"))) + (setq erc--isupport-params (make-hash-table)) + + ;; Uses fallback values when no PREFIX parameter yet received, thus + ;; ensuring caller can use slot accessors immediately intead of + ;; checking if null beforehand. + (should-not erc--parsed-prefix) + (should (equal (erc--parsed-prefix) + #s(erc--parsed-prefix nil "qaohv" "~&@%+" + ((?q . ?~) (?a . ?&) + (?o . ?@) (?h . ?%) (?v . ?+))))) + (let ((cached (should erc--parsed-prefix))) + (should (eq (erc--parsed-prefix) cached))) + + ;; Cache broken. (Notice not setting `erc--parsed-prefix' to nil). + (setq erc-server-parameters '(("PREFIX" . "(Yqaohv)!~&@%+"))) (let ((proc erc-server-process) (expected '((?Y . ?!) (?q . ?~) (?a . ?&) @@ -657,33 +670,33 @@ erc--parse-prefix (with-temp-buffer (erc-mode) (setq erc-server-process proc) - (should (equal expected (erc--parse-prefix)))) + (should (equal expected + (erc--parsed-prefix-alist (erc--parsed-prefix))))) (should (equal expected (erc--parsed-prefix-alist erc--parsed-prefix))) (setq cached erc--parsed-prefix) (should (equal cached - #s(erc--parsed-prefix ("(Yqaohv)!~&@%+") - "Yqaohv" "!~&@%+" + #s(erc--parsed-prefix ("(Yqaohv)!~&@%+") "Yqaohv" "!~&@%+" ((?Y . ?!) (?q . ?~) (?a . ?&) (?o . ?@) (?h . ?%) (?v . ?+))))) ;; Second target buffer reuses cached value. (with-temp-buffer (erc-mode) (setq erc-server-process proc) - (should (eq (erc--parsed-prefix-alist cached) (erc--parse-prefix)))) + (should (eq cached (erc--parsed-prefix)))) ;; New value computed when cache broken. (puthash 'PREFIX (list "(Yqaohv)!~&@%+") erc--isupport-params) (with-temp-buffer (erc-mode) (setq erc-server-process proc) - (should-not (eq (erc--parsed-prefix-alist cached) (erc--parse-prefix))) + (should-not (eq cached (erc--parsed-prefix))) (should (equal (erc--parsed-prefix-alist (erc-with-server-buffer erc--parsed-prefix)) expected))))) -;; This tests exists to prove legacy behavior in order to incorporate -;; it as a fallback in the 5.6+ replacement. +;; This exists as a reference to assert legacy behavior in order to +;; preserve and incorporate it as a fallback in the 5.6+ replacement. (ert-deftest erc-parse-modes () (with-suppressed-warnings ((obsolete erc-parse-modes)) (should (equal (erc-parse-modes "+u") '(("u") nil nil))) @@ -712,9 +725,10 @@ erc--update-channel-modes erc--target (erc--target-from-string "#test")) (erc-tests--set-fake-server-process "sleep" "1") - (let (calls) + (let ((orig-handle-fn (symbol-function 'erc--handle-channel-mode)) + calls) (cl-letf (((symbol-function 'erc--handle-channel-mode) - (lambda (&rest r) (push r calls))) + (lambda (&rest r) (push r calls) (apply orig-handle-fn r))) ((symbol-function 'erc-update-mode-line) #'ignore)) (ert-info ("Unknown user not created") @@ -734,40 +748,99 @@ erc--update-channel-modes (should-not (erc-channel-user-op-p "bob"))) (ert-info ("Unknown nullary added and removed") + (should-not erc--channel-modes) (should-not erc-channel-modes) (erc--update-channel-modes "+u") (should (equal erc-channel-modes '("u"))) + (should (eq t (gethash ?u erc--channel-modes))) + (should (equal (pop calls) '(?d ?u t nil))) (erc--update-channel-modes "-u") + (should (equal (pop calls) '(?d ?u nil nil))) + (should-not (gethash ?u erc--channel-modes)) (should-not erc-channel-modes) (should-not calls)) (ert-info ("Fallback for Type B includes mode letter k") (erc--update-channel-modes "+k" "h2") - (should (equal (pop calls) '(b ?k t "h2"))) + (should (equal (pop calls) '(?b ?k t "h2"))) (should-not erc-channel-modes) + (should (equal "h2" (gethash ?k erc--channel-modes))) (erc--update-channel-modes "-k" "*") - (should (equal (pop calls) '(b ?k nil "*"))) + (should (equal (pop calls) '(?b ?k nil "*"))) + (should-not calls) + (should-not (gethash ?k erc--channel-modes)) (should-not erc-channel-modes)) (ert-info ("Fallback for Type C includes mode letter l") (erc--update-channel-modes "+l" "3") - (should (equal (pop calls) '(c ?l t "3"))) + (should (equal (pop calls) '(?c ?l t "3"))) (should-not erc-channel-modes) + (should (equal "3" (gethash ?l erc--channel-modes))) (erc--update-channel-modes "-l" nil) - (should (equal (pop calls) '(c ?l nil nil))) + (should (equal (pop calls) '(?c ?l nil nil))) + (should-not (gethash ?l erc--channel-modes)) (should-not erc-channel-modes)) (ert-info ("Advertised supersedes heuristics") (setq erc-server-parameters '(("PREFIX" . "(ov)@+") - ("CHANMODES" . "eIbq,k,flj,CFLMPQRSTcgimnprstuz"))) + ;; Add phony 5th type for this CHANMODES value for + ;; robustness in case some server gets creative. + ("CHANMODES" . "eIbq,k,flj,CFLMPQRSTcgimnprstuz,FAKE"))) (erc--update-channel-modes "+qu" "fool!*@*") - (should (equal (pop calls) '(a ?q t "fool!*@*"))) + (should (equal (pop calls) '(?d ?u t nil))) + (should (equal (pop calls) '(?a ?q t "fool!*@*"))) + (should (equal "fool!*@*" (gethash ?q erc--channel-modes))) + (should (eq t (gethash ?u erc--channel-modes))) (should (equal erc-channel-modes '("u"))) (should-not (erc-channel-user-owner-p "bob"))) (should-not calls)))) +(ert-deftest erc--update-user-modes () + (let ((erc--user-modes (list ?a))) + (should (equal (erc--update-user-modes "+a") '(?a))) + (should (equal (erc--update-user-modes "-b") '(?a))) + (should (equal erc--user-modes '(?a)))) + + (let ((erc--user-modes (list ?b))) + (should (equal (erc--update-user-modes "+ac") '(?a ?b ?c))) + (should (equal (erc--update-user-modes "+a-bc") '(?a))) + (should (equal erc--user-modes '(?a))))) + +(ert-deftest erc--user-modes () + (let ((erc--user-modes '(?a ?b))) + (should (equal (erc--user-modes) '(?a ?b))) + (should (equal (erc--user-modes 'string) "ab")) + (should (equal (erc--user-modes 'strings) '("a" "b"))) + (should (equal (erc--user-modes '?+) "+ab")))) + +(ert-deftest erc--parse-user-modes () + (should (equal (erc--parse-user-modes "a" '(?a)) '(() ()))) + (should (equal (erc--parse-user-modes "+a" '(?a)) '(() ()))) + (should (equal (erc--parse-user-modes "a" '()) '((?a) ()))) + (should (equal (erc--parse-user-modes "+a" '()) '((?a) ()))) + (should (equal (erc--parse-user-modes "-a" '()) '(() ()))) + (should (equal (erc--parse-user-modes "-a" '(?a)) '(() (?a)))) + + (should (equal (erc--parse-user-modes "+a-b" '(?a)) '(() ()))) + (should (equal (erc--parse-user-modes "+a-b" '(?b)) '((?a) (?b)))) + (should (equal (erc--parse-user-modes "+ab-c" '(?b)) '((?a) ()))) + (should (equal (erc--parse-user-modes "+ab-c" '(?b ?c)) '((?a) (?c)))) + (should (equal (erc--parse-user-modes "+a-c+b" '(?b ?c)) '((?a) (?c)))) + (should (equal (erc--parse-user-modes "-c+ab" '(?b ?c)) '((?a) (?c)))) + + ;; Param `extrap' returns groups of redundant chars. + (should (equal (erc--parse-user-modes "+a" '() t) '((?a) () () ()))) + (should (equal (erc--parse-user-modes "+a" '(?a) t) '(() () (?a) ()))) + (should (equal (erc--parse-user-modes "-a" '() t) '(() () () (?a)))) + (should (equal (erc--parse-user-modes "-a" '(?a) t) '(() (?a) () ()))) + + (should (equal (erc--parse-user-modes "+a-b" '(?a) t) '(() () (?a) (?b)))) + (should (equal (erc--parse-user-modes "-b+a" '(?a) t) '(() () (?a) (?b)))) + (should (equal (erc--parse-user-modes "+a-b" '(?b) t) '((?a) (?b) () ()))) + (should (equal (erc--parse-user-modes "-b+a" '(?b) t) '((?a) (?b) () ())))) + (ert-deftest erc--parse-isupport-value () (should (equal (erc--parse-isupport-value "a,b") '("a" "b"))) (should (equal (erc--parse-isupport-value "a,b,c") '("a" "b" "c"))) -- 2.41.0