diff options
| author | F. Jason Park | 2025-10-26 06:51:59 -0700 |
|---|---|---|
| committer | F. Jason Park | 2025-10-27 19:29:44 -0700 |
| commit | 44cdb65cf3d98f9d6706419a5cf866ff2df79019 (patch) | |
| tree | c64eb6bbfcae08be4e7bc3c33658643099a39c40 | |
| parent | 2fa768dd4b37319a45761e7c0f02347f32fe8cc6 (diff) | |
| download | emacs-44cdb65cf3d98f9d6706419a5cf866ff2df79019.tar.gz emacs-44cdb65cf3d98f9d6706419a5cf866ff2df79019.zip | |
Fix regression involving erc-channel-user accessors
* etc/ERC-NEWS: New section for ERC 5.6.2 and new entry mentioning
slight change in `erc-channel-user' accessor behavior.
* lisp/erc/erc-common.el (erc-channel-user): Change type for status slot
to natnum from integer.
* lisp/erc/erc.el (erc-channel-user-signal-if-status-unknown): New
variable.
(erc--define-channel-user-status-compat-getter): Only use fallback in
Non-ERC buffers because "obviously" a status flag is unusable if the
server doesn't advertise it or, rather, advertises nonsupport via its
absence. This regression was introduced in ERC 5.6.
(erc-channel-begin-receiving-names): Sharp-quote function name.
(erc--get-prefix-flag): Mention in doc that a return value of nil can
also mean the status flag is not supported by the server.
* test/lisp/erc/erc-tests.el (erc--parsed-prefix): Show that it returns
nil in a non-ERC buffer.
(erc-tests--make-combinations)
(erc-tests--with-channel-user-status-accessors): New functions.
(erc-channel-user/status-accessors/solo/default)
(erc-channel-user/status-accessors/solo/ov)
(erc-channel-user/status-accessors/multi/default)
(erc-channel-user/status-accessors/multi/ov): New tests.
(Bug#67220)
| -rw-r--r-- | etc/ERC-NEWS | 14 | ||||
| -rw-r--r-- | lisp/erc/erc-common.el | 2 | ||||
| -rw-r--r-- | lisp/erc/erc.el | 46 | ||||
| -rw-r--r-- | test/lisp/erc/erc-tests.el | 158 |
4 files changed, 203 insertions, 17 deletions
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 513ed8f706d..932b7a58aa7 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS | |||
| @@ -12,6 +12,20 @@ extensible IRC (Internet Relay Chat) client distributed with | |||
| 12 | GNU Emacs since Emacs version 22.1. | 12 | GNU Emacs since Emacs version 22.1. |
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | * Changes in ERC 5.6.2 | ||
| 16 | |||
| 17 | ** Changes in the library API. | ||
| 18 | |||
| 19 | *** Accessors like 'erc-channel-user-voice' may ignore assignments. | ||
| 20 | ERC now silently ignores attempts to enable certain status flags on | ||
| 21 | 'erc-channel-user' objects if the connection's "PREFIX" parameter omits | ||
| 22 | them. In the future, ERC will likely signal an error if such an attempt | ||
| 23 | is made. Users can preview this potentially disruptive behavior by | ||
| 24 | setting the new variable 'erc-channel-user-signal-if-status-unknown' to | ||
| 25 | t. This change stems from a bug fix for a regression affecting ERC 5.6 | ||
| 26 | and 5.6.1 in which these accessors mishandled unsupported flags. | ||
| 27 | |||
| 28 | |||
| 15 | * Changes in ERC 5.6.1 | 29 | * Changes in ERC 5.6.1 |
| 16 | 30 | ||
| 17 | ** Option 'erc-truncate-padding-size' controls truncation frequency. | 31 | ** Option 'erc-truncate-padding-size' controls truncation frequency. |
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index d293e6ba878..e383e92c7ff 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el | |||
| @@ -100,7 +100,7 @@ ERC only refolds `string', never `substxt'.")) | |||
| 100 | :named) | 100 | :named) |
| 101 | "Object containing channel-specific data for a single user." | 101 | "Object containing channel-specific data for a single user." |
| 102 | ;; voice halfop op admin owner | 102 | ;; voice halfop op admin owner |
| 103 | (status 0 :type integer) | 103 | (status 0 :type natnum) |
| 104 | ;; Last message time (in the form of the return value of | 104 | ;; Last message time (in the form of the return value of |
| 105 | ;; (current-time) | 105 | ;; (current-time) |
| 106 | ;; | 106 | ;; |
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index e26ee8728a3..271c5d5fcf8 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el | |||
| @@ -684,6 +684,11 @@ Also remove members from the server table if this was their only buffer." | |||
| 684 | (funcall original-function nick user)))))) | 684 | (funcall original-function nick user)))))) |
| 685 | (erc-remove-channel-users))) | 685 | (erc-remove-channel-users))) |
| 686 | 686 | ||
| 687 | (defvar erc-channel-user-signal-if-status-unknown nil | ||
| 688 | "If non-nil ERC signals before setting an unadvertized status prefix. | ||
| 689 | But only in ERC buffers. Otherwise, if nil, accessors like | ||
| 690 | `erc-channel-user-halfop' ignore such attempts and return nil.") | ||
| 691 | |||
| 687 | (defmacro erc--define-channel-user-status-compat-getter (name c d) | 692 | (defmacro erc--define-channel-user-status-compat-getter (name c d) |
| 688 | "Define accessor with gv getter for historical `erc-channel-user' slot NAME. | 693 | "Define accessor with gv getter for historical `erc-channel-user' slot NAME. |
| 689 | Expect NAME to be a string, C to be its traditionally associated letter, | 694 | Expect NAME to be a string, C to be its traditionally associated letter, |
| @@ -691,19 +696,30 @@ and D to be its fallback power-of-2 integer for non-ERC buffers. Unlike | |||
| 691 | pre-ERC-5.6 accessors, do not bother generating a compiler macro for | 696 | pre-ERC-5.6 accessors, do not bother generating a compiler macro for |
| 692 | inlining calls to these adapters." | 697 | inlining calls to these adapters." |
| 693 | `(defun ,(intern (concat "erc-channel-user-" name)) (u) | 698 | `(defun ,(intern (concat "erc-channel-user-" name)) (u) |
| 694 | ,(format "Get equivalent of pre-5.6 `%s' slot for `erc-channel-user'." | 699 | ,(concat |
| 695 | name) | 700 | "Get equivalent of pre-5.6 `" name "' slot for `erc-channel-user'." |
| 696 | (declare (gv-setter (lambda (v) | 701 | "\nUse a fallback value in non-ERC buffers. Treat an unadvertised" |
| 697 | (macroexp-let2 nil v v | 702 | "\nstatus according to `erc-channel-user-signal-if-status-unknown'.") |
| 698 | (,'\`(let ((val (erc-channel-user-status ,',u)) | 703 | (declare (gv-setter |
| 699 | (n (or (erc--get-prefix-flag ,c) ,d))) | 704 | (lambda (v) |
| 700 | (setf (erc-channel-user-status ,',u) | 705 | (macroexp-let2 nil v v |
| 701 | (if ,',v | 706 | (,'\`(let* ((val (erc-channel-user-status ,',u)) |
| 702 | (logior val n) | 707 | (p (erc--parsed-prefix)) |
| 703 | (logand val (lognot n)))) | 708 | (n (if p (or (erc--get-prefix-flag ,c p) 0) ,d)) |
| 704 | ,',v)))))) | 709 | (nop (and p ,',v (zerop n))) ; unsupportedp |
| 705 | (let ((n (or (erc--get-prefix-flag ,c) ,d))) | 710 | (rv (and (not nop) ,',v))) |
| 706 | (= n (logand n (erc-channel-user-status u)))))) | 711 | (when (and nop |
| 712 | erc-channel-user-signal-if-status-unknown) | ||
| 713 | (error "Unsupported status prefix: %c" ,c)) | ||
| 714 | (unless nop | ||
| 715 | (setf (erc-channel-user-status ,',u) | ||
| 716 | (if ,',v | ||
| 717 | (logior val n) | ||
| 718 | (logand val (lognot n))))) | ||
| 719 | rv)))))) | ||
| 720 | (let* ((p (erc--parsed-prefix)) | ||
| 721 | (n (if p (erc--get-prefix-flag ,c p) ,d))) | ||
| 722 | (and n (= n (logand n (erc-channel-user-status u))))))) | ||
| 707 | 723 | ||
| 708 | (erc--define-channel-user-status-compat-getter "voice" ?v 1) | 724 | (erc--define-channel-user-status-compat-getter "voice" ?v 1) |
| 709 | (erc--define-channel-user-status-compat-getter "halfop" ?h 2) | 725 | (erc--define-channel-user-status-compat-getter "halfop" ?h 2) |
| @@ -7090,7 +7106,7 @@ Used when a channel names list is about to be received. Should | |||
| 7090 | be called with the current buffer set to the channel buffer. | 7106 | be called with the current buffer set to the channel buffer. |
| 7091 | 7107 | ||
| 7092 | See also `erc-channel-end-receiving-names'." | 7108 | See also `erc-channel-end-receiving-names'." |
| 7093 | (setq erc-channel-new-member-names (make-hash-table :test 'equal))) | 7109 | (setq erc-channel-new-member-names (make-hash-table :test #'equal))) |
| 7094 | 7110 | ||
| 7095 | (defun erc-channel-end-receiving-names () | 7111 | (defun erc-channel-end-receiving-names () |
| 7096 | "Internal function. | 7112 | "Internal function. |
| @@ -7142,7 +7158,7 @@ stand-in from the fallback value \"(qaohv)~&@%+\"." | |||
| 7142 | :alist (nreverse alist))))) | 7158 | :alist (nreverse alist))))) |
| 7143 | 7159 | ||
| 7144 | (defun erc--get-prefix-flag (char &optional parsed-prefix from-prefix-p) | 7160 | (defun erc--get-prefix-flag (char &optional parsed-prefix from-prefix-p) |
| 7145 | "Return numeric rank for CHAR or nil if unknown. | 7161 | "Return numeric rank for CHAR or nil if unknown or unsupported. |
| 7146 | For example, given letters \"qaohv\" return 1 for ?v, 2 for ?h, | 7162 | For example, given letters \"qaohv\" return 1 for ?v, 2 for ?h, |
| 7147 | and 4 for ?o, etc. If given, expect PARSED-PREFIX to be a | 7163 | and 4 for ?o, etc. If given, expect PARSED-PREFIX to be a |
| 7148 | `erc--parsed-prefix' object. With FROM-PREFIX-P, expect CHAR to | 7164 | `erc--parsed-prefix' object. With FROM-PREFIX-P, expect CHAR to |
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 1b486c68584..7ad2d268fa3 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el | |||
| @@ -688,6 +688,9 @@ | |||
| 688 | (should-not (erc--parse-nuh "abc\nde!fg@xy"))) | 688 | (should-not (erc--parse-nuh "abc\nde!fg@xy"))) |
| 689 | 689 | ||
| 690 | (ert-deftest erc--parsed-prefix () | 690 | (ert-deftest erc--parsed-prefix () |
| 691 | ;; Effectively a no-op in a non-ERC buffer. | ||
| 692 | (should-not (erc--parsed-prefix)) | ||
| 693 | |||
| 691 | (erc-tests-common-make-server-buf (buffer-name)) | 694 | (erc-tests-common-make-server-buf (buffer-name)) |
| 692 | 695 | ||
| 693 | ;; Uses fallback values when no PREFIX parameter yet received, thus | 696 | ;; Uses fallback values when no PREFIX parameter yet received, thus |
| @@ -817,6 +820,156 @@ | |||
| 817 | (should-not (erc--cusr-status-p cusr ?v)) | 820 | (should-not (erc--cusr-status-p cusr ?v)) |
| 818 | (should-not (erc--cusr-status-p cusr ?q))))) | 821 | (should-not (erc--cusr-status-p cusr ?q))))) |
| 819 | 822 | ||
| 823 | (defun erc-tests--make-combinations (flags) | ||
| 824 | "Return a list of all combinations in FLAGS, preserving order." | ||
| 825 | (let* ((flags (apply #'vector flags)) | ||
| 826 | (n (length flags)) | ||
| 827 | (max (1- (ash 1 n))) | ||
| 828 | (mask 0) | ||
| 829 | (out ())) | ||
| 830 | (while (<= (cl-incf mask) max) | ||
| 831 | (let ((i 0) | ||
| 832 | (subset ())) | ||
| 833 | (while (< i n) | ||
| 834 | (unless (zerop (logand mask (ash 1 i))) | ||
| 835 | (push (aref flags i) subset)) | ||
| 836 | (cl-incf i)) | ||
| 837 | (when (cdr subset) | ||
| 838 | (push (nreverse subset) out)))) | ||
| 839 | out)) | ||
| 840 | |||
| 841 | (defun erc-tests--with-channel-user-status-accessors (test) | ||
| 842 | (erc-tests-common-make-server-buf) | ||
| 843 | |||
| 844 | (let* ((u (make-erc-channel-user)) | ||
| 845 | (get (lambda (letter) | ||
| 846 | (pcase letter | ||
| 847 | (?v (erc-channel-user-voice u)) | ||
| 848 | (?h (erc-channel-user-halfop u)) | ||
| 849 | (?o (erc-channel-user-op u)) | ||
| 850 | (?a (erc-channel-user-admin u)) | ||
| 851 | (?q (erc-channel-user-owner u))))) | ||
| 852 | |||
| 853 | (set (lambda (letter val) | ||
| 854 | (pcase letter | ||
| 855 | (?v (setf (erc-channel-user-voice u) val)) | ||
| 856 | (?h (setf (erc-channel-user-halfop u) val)) | ||
| 857 | (?o (setf (erc-channel-user-op u) val)) | ||
| 858 | (?a (setf (erc-channel-user-admin u) val)) | ||
| 859 | (?q (setf (erc-channel-user-owner u) val))))) | ||
| 860 | |||
| 861 | (assert-null | ||
| 862 | (lambda (&rest letters) | ||
| 863 | (dolist (letter letters) | ||
| 864 | (ert-info ((format "Assert null: %c" letter)) | ||
| 865 | (should-not (funcall get letter)))))) | ||
| 866 | |||
| 867 | (assert-set | ||
| 868 | (lambda (letter &optional nop) | ||
| 869 | (ert-info ((format "Assert: %c%s" letter (if nop " (no-op)" ""))) | ||
| 870 | (should-not (funcall get letter)) | ||
| 871 | (if (and nop erc-channel-user-signal-if-status-unknown) | ||
| 872 | (should-error (funcall set letter t)) | ||
| 873 | ;; If the flag is unsupported, always return nil, | ||
| 874 | ;; and don't set anything, otherwise, return t. | ||
| 875 | (let ((rv (funcall set letter t))) | ||
| 876 | (ert-info ((format "Set: %S" rv)) | ||
| 877 | (should (xor rv nop))))) | ||
| 878 | (let ((rv (funcall get letter))) | ||
| 879 | (ert-info ((format "Get: %S" rv)) | ||
| 880 | (should (xor rv nop))))))) | ||
| 881 | |||
| 882 | (assert-solo | ||
| 883 | (lambda (letter &optional nop) | ||
| 884 | (setf (erc-channel-user-status u) 0) ; clear | ||
| 885 | (funcall assert-set letter nop) | ||
| 886 | (apply assert-null (seq-difference '(?v ?h ?o ?a ?q) | ||
| 887 | (list letter))))) | ||
| 888 | |||
| 889 | (assert-multi | ||
| 890 | (lambda (&rest supported) | ||
| 891 | ;; Set all defined flags from smallest to largest rank. | ||
| 892 | (dolist (flags (erc-tests--make-combinations '(?v ?h ?o ?a ?q))) | ||
| 893 | (setf (erc-channel-user-status u) 0) | ||
| 894 | (ert-info ((let ((print-integers-as-characters t)) | ||
| 895 | (format "Multi %S" (list :flags flags | ||
| 896 | :supported supported)))) | ||
| 897 | (let ((seen-supported ()) | ||
| 898 | (seen-unsupported ())) | ||
| 899 | (dolist (a flags) | ||
| 900 | (let ((supportedp (memq a supported))) | ||
| 901 | (push a (if supportedp seen-supported seen-unsupported)) | ||
| 902 | (funcall assert-set a (not supportedp)) | ||
| 903 | ;; Addition of new flag has not corrupted others. | ||
| 904 | (dolist (aa seen-supported) | ||
| 905 | (ert-info ((format "Seen supported: %s %c" u aa)) | ||
| 906 | (should (funcall get aa)))) | ||
| 907 | (dolist (aa `(,@seen-unsupported ,@(cdr (memq a flags)))) | ||
| 908 | (should-not (funcall get aa)))))) | ||
| 909 | ;; Unset in reverse, although not doing so is valid. | ||
| 910 | (setq flags (nreverse flags)) | ||
| 911 | (let ((seen ())) | ||
| 912 | (while-let ((b (pop flags))) | ||
| 913 | (ert-info ((format "Unsetting: %S %c" u b)) | ||
| 914 | (should-not (funcall set b nil)) | ||
| 915 | (dolist (bb (push b seen)) | ||
| 916 | (ert-info ((format "Seen unset: %c" bb)) | ||
| 917 | (should-not (funcall get bb)))) | ||
| 918 | (dolist (bb flags) | ||
| 919 | (ert-info ((format "Unseen set: %c" bb)) | ||
| 920 | (if (memq bb supported) | ||
| 921 | (should (funcall get bb)) | ||
| 922 | (should-not (funcall get bb))))))))))))) | ||
| 923 | |||
| 924 | ;; Run the same test twice, with compat flag nil and non-nil. | ||
| 925 | (let ((erc-channel-user-signal-if-status-unknown nil)) | ||
| 926 | (funcall test assert-null assert-set assert-solo assert-multi)) | ||
| 927 | |||
| 928 | (ert-info ("With `erc-channel-user-signal-if-status-unknown'") | ||
| 929 | (setf (erc-channel-user-status u) 0) ; clear | ||
| 930 | (let ((erc-channel-user-signal-if-status-unknown t)) | ||
| 931 | (funcall test assert-null assert-set assert-solo assert-multi))) | ||
| 932 | |||
| 933 | (erc-tests-common-kill-buffers))) | ||
| 934 | |||
| 935 | (ert-deftest erc-channel-user/status-accessors/solo/default () | ||
| 936 | (erc-tests--with-channel-user-status-accessors | ||
| 937 | (lambda (assert-null _assert-set assert-solo _assert-multi) | ||
| 938 | |||
| 939 | (ert-info ("Baseline") | ||
| 940 | (funcall assert-null ?v ?h ?o ?a ?q)) | ||
| 941 | |||
| 942 | (ert-info ("+v") (funcall assert-solo ?v)) | ||
| 943 | (ert-info ("+h") (funcall assert-solo ?h)) | ||
| 944 | (ert-info ("+o") (funcall assert-solo ?o)) | ||
| 945 | (ert-info ("+a") (funcall assert-solo ?a)) | ||
| 946 | (ert-info ("+q") (funcall assert-solo ?q))))) | ||
| 947 | |||
| 948 | (ert-deftest erc-channel-user/status-accessors/solo/ov () | ||
| 949 | (erc-tests--with-channel-user-status-accessors | ||
| 950 | (lambda (assert-null _assert-set assert-solo _assert-multi) | ||
| 951 | (erc-tests-common-simulate-line ":irc.gnu.org 005 tester PREFIX=(ov)@+") | ||
| 952 | |||
| 953 | (ert-info ("Baseline") | ||
| 954 | (funcall assert-null ?v ?h ?o ?a ?q)) | ||
| 955 | |||
| 956 | (ert-info ("+v") (funcall assert-solo ?v)) | ||
| 957 | (ert-info ("+h (unknown)") (funcall assert-solo ?h 'nop)) | ||
| 958 | (ert-info ("+o") (funcall assert-solo ?o)) | ||
| 959 | (ert-info ("+a (unknown)") (funcall assert-solo ?a 'nop)) | ||
| 960 | (ert-info ("+q (unknown)") (funcall assert-solo ?q 'nop))))) | ||
| 961 | |||
| 962 | (ert-deftest erc-channel-user/status-accessors/multi/default () | ||
| 963 | (erc-tests--with-channel-user-status-accessors | ||
| 964 | (lambda (_assert-null _assert-set _assert-solo assert-multi) | ||
| 965 | (funcall assert-multi ?v ?h ?o ?a ?q)))) | ||
| 966 | |||
| 967 | (ert-deftest erc-channel-user/status-accessors/multi/ov () | ||
| 968 | (erc-tests--with-channel-user-status-accessors | ||
| 969 | (lambda (_assert-null _assert-set _assert-solo assert-multi) | ||
| 970 | (erc-tests-common-simulate-line ":irc.gnu.org 005 tester PREFIX=(ov)@+") | ||
| 971 | (funcall assert-multi ?v ?o)))) | ||
| 972 | |||
| 820 | ;; This exists as a reference to assert legacy behavior in order to | 973 | ;; This exists as a reference to assert legacy behavior in order to |
| 821 | ;; preserve and incorporate it as a fallback in the 5.6+ replacement. | 974 | ;; preserve and incorporate it as a fallback in the 5.6+ replacement. |
| 822 | (ert-deftest erc-parse-modes () | 975 | (ert-deftest erc-parse-modes () |
| @@ -3489,8 +3642,11 @@ | |||
| 3489 | (when noninteractive | 3642 | (when noninteractive |
| 3490 | (erc-tests-common-kill-buffers))) | 3643 | (erc-tests-common-kill-buffers))) |
| 3491 | 3644 | ||
| 3645 | ;; For legacy accessors, like `erc-channel-user-halfop', this test only | ||
| 3646 | ;; demonstrates compat-oriented behavior in a non-ERC buffer. See | ||
| 3647 | ;; `erc-tests--with-channel-user-status-accessors' based tests for | ||
| 3648 | ;; behavior in ERC buffers, both fallback and ISUPPORT-defined. | ||
| 3492 | (ert-deftest erc-channel-user () | 3649 | (ert-deftest erc-channel-user () |
| 3493 | ;; Traditional and alternate constructor swapped for compatibility. | ||
| 3494 | (should (= 0 (erc-channel-user-status (erc-channel-user--make)))) | 3650 | (should (= 0 (erc-channel-user-status (erc-channel-user--make)))) |
| 3495 | (should-not (erc-channel-user-last-message-time (erc-channel-user--make))) | 3651 | (should-not (erc-channel-user-last-message-time (erc-channel-user--make))) |
| 3496 | 3652 | ||