branch: externals/shift-number
commit 46c277f77804343d8ac7f10855d4aff43341df30
Author: Campbell Barton <[email protected]>
Commit: Campbell Barton <[email protected]>

    Tests: add ERT test suite
    
    Add comprehensive ERT test suite with 91 tests covering:
    - Basic integer operations test increment, decrement, and delta values.
    - Negative number handling tests the shift-number-negative option.
    - Leading zeros preservation tests padding and overflow behavior.
    - Width change tests cover digit gain and reduction scenarios.
    - Cursor position tests verify edge cases at number boundaries.
    - No-operation tests ensure proper error conditions.
    - Sign context tests handle numbers in mathematical expressions.
    - Region and rectangle mode tests verify multi-number operations.
    - Motion option tests verify shift-number-motion behavior.
    - Large number tests ensure no overflow issues.
    - Context tests verify numbers in various buffer positions.
    - Decimal number tests distinguish integer vs fractional parts.
    - Scientific notation tests cover mantissa and exponent handling.
    - Version number tests handle multi-component formats.
    
    Test infrastructure includes:
    - The with-shift-number-test macro handles buffer setup.
    - The cursor-marker function enables position verification.
    - The should-error-with-message macro tests error conditions.
    - The shell script runner (shift-number-tests.sh) enables batch execution.
    - Makefile integration supports the EMACS variable for flexibility.
---
 Makefile                    |   23 +-
 tests/shift-number-tests.el | 1096 +++++++++++++++++++++++++++++++++++++++++++
 tests/shift-number-tests.sh |    7 +
 3 files changed, 1111 insertions(+), 15 deletions(-)

diff --git a/Makefile b/Makefile
index 17ca806ac7..b23e8347a4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,21 +1,14 @@
-# This is not a full-featured Makefile and it is not intended to be used
-# to install 'shift-number' package to your system.  Its only purpose is
-# to byte-compile "shift-number.el" (using 'make') to make sure that
-# there are no compilation warnings.
+# SPDX-License-Identifier: GPL-3.0-or-later
+# This Makefile is for convenience only; it is not needed for building the 
package.
 
-EMACS = emacs
+EMACS ?= emacs
 
-LOAD_PATH = -L .
-EMACS_BATCH = $(EMACS) -batch -Q $(LOAD_PATH)
+.PHONY: all test clean
 
-ELS = shift-number.el
-ELCS = $(ELS:.el=.elc)
+all: test
 
-all: $(ELCS)
-
-%.elc: %.el
-       @printf "Compiling $<\n"
-       @$(EMACS_BATCH) -f batch-byte-compile $<
+test:
+       ./tests/shift-number-tests.sh
 
 clean:
-       $(RM) $(ELCS)
+       rm -f *.elc
diff --git a/tests/shift-number-tests.el b/tests/shift-number-tests.el
new file mode 100644
index 0000000000..b8ffd98527
--- /dev/null
+++ b/tests/shift-number-tests.el
@@ -0,0 +1,1096 @@
+;;; shift-number-tests.el --- Testing -*- lexical-binding: t -*-
+
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;;; Commentary:
+
+;; See: `shift-number-tests.sh' for launching this script.
+
+(require 'ert)
+(require 'rect)
+
+;;; Code:
+
+(defvar shift-number-tests-basedir
+  (concat (file-name-directory load-file-name) "..")
+  "Base directory for shift-number tests.")
+(add-to-list 'load-path shift-number-tests-basedir)
+(require 'shift-number)
+
+;; ---------------------------------------------------------------------------
+;; Internal Functions/Macros
+
+(defun buffer-reset-text (initial-buffer-text)
+  "Initialize buffer with INITIAL-BUFFER-TEXT."
+  (erase-buffer)
+  ;; Don't move the cursor.
+  (save-excursion (insert initial-buffer-text)))
+
+(defmacro with-shift-number-test (initial-buffer-text &rest body)
+  "Run BODY with messages inhibited, setting buffer text to 
INITIAL-BUFFER-TEXT."
+  (declare (indent 1))
+  `(with-temp-buffer
+     (let ((inhibit-message t))
+       (buffer-reset-text ,initial-buffer-text)
+       ,@body)))
+
+(defun cursor-marker ()
+  "Insert a | character at point to mark cursor position for test 
verification."
+  (insert "|"))
+
+(defmacro should-error-with-message (form error-type expected-message)
+  "Assert FORM signals an error of ERROR-TYPE with EXPECTED-MESSAGE."
+  (declare (indent 1))
+  (let ((err-sym (make-symbol "err")))
+    `(let ((,err-sym (should-error ,form :type ,error-type)))
+       (should (equal ,expected-message (error-message-string ,err-sym))))))
+
+
+;; ---------------------------------------------------------------------------
+;; Basic Integer Tests
+
+(ert-deftest simple-increment ()
+  "Check a single number increments."
+  (let ((text-initial "1")
+        (text-expected "|2"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest simple-decrement ()
+  "Check a single number decrements."
+  (let ((text-initial "2")
+        (text-expected "|1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest simple-increment-by-amount ()
+  "Check incrementing by a specific amount."
+  (let ((text-initial "5")
+        (text-expected "|15"))
+    (with-shift-number-test text-initial
+      (shift-number-up 10)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest simple-decrement-by-amount ()
+  "Check decrementing by a specific amount."
+  (let ((text-initial "15")
+        (text-expected "|5"))
+    (with-shift-number-test text-initial
+      (shift-number-down 10)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest delta-zero ()
+  "Check that delta of zero leaves number unchanged."
+  (let ((text-initial "42")
+        (text-expected "|42"))
+    (with-shift-number-test text-initial
+      (shift-number-up 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest zero-increment ()
+  "Check that single digit zero increments correctly."
+  (let ((text-initial "0")
+        (text-expected "|1"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-delta-up ()
+  "Check that negative delta decrements when using shift-number-up."
+  (let ((text-initial "10")
+        (text-expected "|7"))
+    (with-shift-number-test text-initial
+      (shift-number-up -3)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-delta-down ()
+  "Check that negative delta increments when using shift-number-down."
+  (let ((text-initial "10")
+        (text-expected "|13"))
+    (with-shift-number-test text-initial
+      (shift-number-down -3)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Negative Number Tests
+
+(ert-deftest simple-to-negative ()
+  "Check a number can decrement to negative."
+  (let ((text-initial "0")
+        (text-expected "|-1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-increment ()
+  "Check a negative number increments correctly."
+  (let ((text-initial "-5")
+        (text-expected "|-4"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-to-positive ()
+  "Check a negative number can become positive."
+  (let ((text-initial "-1")
+        (text-expected "|1"))
+    (with-shift-number-test text-initial
+      (shift-number-up 2)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-to-zero ()
+  "Check a negative number can become zero."
+  (let ((text-initial "-1")
+        (text-expected "|0"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-two-digit-to-one-digit ()
+  "Check that incrementing -10 to -9 reduces digit count correctly."
+  (let ((text-initial "-10")
+        (text-expected "|-9"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-disabled-at-zero ()
+  "Check that decrementing below zero is prevented when shift-number-negative 
is nil."
+  (let ((text-initial "0")
+        (text-expected "|0")
+        (shift-number-negative nil))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-disabled-existing-negative ()
+  "Check that existing negative is treated as unsigned when 
shift-number-negative is nil.
+The minus sign is ignored, so -5 becomes -6 (5 incremented to 6)."
+  (let ((text-initial "-5")
+        (text-expected "|-6")
+        (shift-number-negative nil))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest negative-disabled-existing-negative-decrement ()
+  "Check that decrementing with shift-number-negative nil treats number as 
unsigned.
+The minus sign is ignored, so -5 is treated as 5, decrementing gives 4, result 
is -4."
+  (let ((text-initial "-5")
+        (text-expected "|-4")
+        (shift-number-negative nil))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest positive-sign-preserved ()
+  "Check that explicit + sign is preserved."
+  (let ((text-initial "+5")
+        (text-expected "|+6"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest positive-sign-to-negative ()
+  "Check that explicit + sign becomes - when crossing zero."
+  (let ((text-initial "+1")
+        (text-expected "|-1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 2)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Leading Zeros / Padding Tests
+
+(ert-deftest leading-zeros-preserved ()
+  "Check that leading zeros are preserved when incrementing."
+  (let ((text-initial "007")
+        (text-expected "|008"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-preserved-decrement ()
+  "Check that leading zeros are preserved when decrementing."
+  (let ((text-initial "010")
+        (text-expected "|009"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-overflow-padded ()
+  "Check that leading zeros are lost when number overflows padded width."
+  (let ((text-initial "099")
+        (text-expected "|100"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-width-gain ()
+  "Check that leading zeros are preserved when number gains a digit within 
width."
+  (let ((text-initial "009")
+        (text-expected "|010"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-negative ()
+  "Check leading zeros with negative numbers."
+  (let ((text-initial "-007")
+        (text-expected "|-006"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-negative-to-zero ()
+  "Check that negative number with leading zeros can become zero."
+  (let ((text-initial "-001")
+        (text-expected "|000"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest leading-zeros-to-negative ()
+  "Check that positive number with leading zeros can become negative."
+  (let ((text-initial "001")
+        (text-expected "|-001"))
+    (with-shift-number-test text-initial
+      (shift-number-down 2)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Width Change Tests
+
+(ert-deftest width-gain ()
+  "Check behavior when number gains a digit."
+  (let ((text-initial "99")
+        (text-expected "|100"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest width-reduction ()
+  "Check behavior when number loses a digit."
+  (let ((text-initial "100")
+        (text-expected "|99"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest width-reduction-negative ()
+  "Check behavior when negative number loses a digit."
+  (let ((text-initial "-100")
+        (text-expected "|-99"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Cursor Position Tests
+
+(ert-deftest cursor-in-middle ()
+  "Check that number is found when cursor is in the middle."
+  (let ((text-initial "12345")
+        (text-expected "12|346"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '3'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-at-end ()
+  "Check that number is found when cursor is at the end."
+  (let ((text-initial "123")
+        (text-expected "12|4"))
+    (with-shift-number-test text-initial
+      (goto-char (point-max))
+      (backward-char 1) ; Position cursor on '3'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-before-number ()
+  "Check that number ahead on line is found when cursor is before it."
+  (let ((text-initial "abc 42")
+        (text-expected "|abc 43"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-between-numbers ()
+  "Check that next number is found when cursor is between numbers."
+  (let ((text-initial "10 20 30")
+        (text-expected "10 |21 30"))
+    (with-shift-number-test text-initial
+      (forward-char 3) ; Position cursor on space after '10'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-on-first-of-multiple ()
+  "Check that first number is incremented when cursor is on it."
+  (let ((text-initial "10 20 30")
+        (text-expected "|11 20 30"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-on-negative-sign ()
+  "Check that number is found when cursor is on the negative sign."
+  (let ((text-initial "-42")
+        (text-expected "|-41"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-on-positive-sign ()
+  "Check that number is found when cursor is on the positive sign."
+  (let ((text-initial "+42")
+        (text-expected "|+43"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest cursor-after-last-digit ()
+  "Check that number is found when cursor is immediately after last digit."
+  (let ((text-initial "123")
+        (text-expected "124|"))
+    (with-shift-number-test text-initial
+      (goto-char (point-max)) ; Position cursor after '3'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; No Operation / Edge Case Tests
+
+(ert-deftest nop-no-number ()
+  "Error when there's no number on the line."
+  (let ((text-initial "abc")
+        (text-expected "|abc"))
+    (with-shift-number-test text-initial
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-non-number-signed ()
+  "Error when minus sign is followed by non-digit."
+  (let ((text-initial "-X")
+        (text-expected "|-X"))
+    (with-shift-number-test text-initial
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-cursor-after-trailing-space ()
+  "Error when cursor is after number with trailing space at end of buffer."
+  (let ((text-initial "123 ")
+        (text-expected "123 |"))
+    (with-shift-number-test text-initial
+      (goto-char (point-max))
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-cursor-after-trailing-space-decrement ()
+  "Error when decrementing with cursor after trailing space."
+  (let ((text-initial "123 ")
+        (text-expected "123 |"))
+    (with-shift-number-test text-initial
+      (goto-char (point-max))
+      (should-error-with-message
+          (shift-number-down 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-blank-line ()
+  "Error on a blank line."
+  (let ((text-initial "")
+        (text-expected "|"))
+    (with-shift-number-test text-initial
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-blank-line-decrement ()
+  "Error on a blank line when decrementing."
+  (let ((text-initial "")
+        (text-expected "|"))
+    (with-shift-number-test text-initial
+      (should-error-with-message
+          (shift-number-down 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-different-line ()
+  "Error - do not increment number on a different line."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "\n"
+                 "42"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "|\n"
+                 "42")))
+    (with-shift-number-test text-initial
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest nop-cursor-after-all-numbers ()
+  "Error when cursor is positioned after all numbers on the line."
+  (let ((text-initial "10 20 30 ")
+        (text-expected "10 20 30 |"))
+    (with-shift-number-test text-initial
+      (goto-char (point-max)) ; Position cursor after trailing space.
+      (should-error-with-message
+          (shift-number-up 1)
+        'error
+        "No number on the current line")
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Sign Context Tests (avoid false negatives in expressions)
+
+(ert-deftest expression-subtraction ()
+  "Check that subtraction expressions don't create false negatives."
+  (let ((text-initial "123-456")
+        (text-expected "123-|457"))
+    (with-shift-number-test text-initial
+      (forward-char 4) ; Position cursor on '4'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest expression-subtraction-first-number ()
+  "Check that first number in subtraction is incremented independently."
+  (let ((text-initial "123-456")
+        (text-expected "|124-456"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest expression-addition ()
+  "Check that addition expressions work correctly."
+  (let ((text-initial "123+456")
+        (text-expected "123+|457"))
+    (with-shift-number-test text-initial
+      (forward-char 4) ; Position cursor on '4'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Region Tests
+
+(ert-deftest region-multiple-numbers ()
+  "Check that all numbers in a region are incremented."
+  (let ((text-initial "1 2 3")
+        (text-expected "2 3 |4"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-decrement-multiple ()
+  "Check that all numbers in a region are decremented."
+  (let ((text-initial "5 6 7")
+        (text-expected "4 5 |6"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-partial ()
+  "Check that only numbers within the region are modified.
+The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 and 5 unchanged."
+  (let ((text-initial "1 2 3 4 5")
+        (text-expected "1 3 4 |4 5"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char 3) ; Position before '2'.
+      (set-mark (point))
+      (forward-char 4) ; Position after '3'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-no-numbers ()
+  "Check that region with no numbers leaves buffer unchanged."
+  (let ((text-initial "abc def ghi")
+        (text-expected "abc def ghi|"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-mixed-signs ()
+  "Check that region with mixed positive and negative numbers works."
+  (let ((text-initial "-5 0 +5")
+        (text-expected "-4 1 +|6"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-multiple-lines ()
+  "Check that region spanning multiple lines increments all numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "1 2\n"
+                 "3 4\n"
+                 "5 6"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "2 3\n"
+                 "4 5\n"
+                 "6 |7")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest region-reversed ()
+  "Check that region works when mark is after point."
+  (let ((text-initial "1 2 3")
+        (text-expected "|2 3 4"))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-max))
+      (set-mark (point))
+      (goto-char (point-min))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Rectangle Mode Tests
+
+(ert-deftest rectangle-single-column ()
+  "Check rectangle mode increments a column of numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "1 0 0\n"
+                 "1 0 0\n"
+                 "1 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "2 0 0\n"
+                 "2 0 0\n"
+                 "|2 0 0")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 4) ; Position at column 0 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-up 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest rectangle-with-negatives ()
+  "Check rectangle mode works with negative numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "-5 0\n"
+                 "-5 0\n"
+                 "-5 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "-4 0\n"
+                 "-4 0\n"
+                 "-|4 0")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 2) ; Position at column 0 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-up 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest rectangle-decrement ()
+  "Check rectangle mode decrements a column of numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "5 0 0\n"
+                 "5 0 0\n"
+                 "5 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "4 0 0\n"
+                 "4 0 0\n"
+                 "|4 0 0")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 4) ; Position at column 0 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-down 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest rectangle-middle-column ()
+  "Check rectangle mode increments a middle column of numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "1 2 3\n"
+                 "1 2 3\n"
+                 "1 2 3"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "1 3 3\n"
+                 "1 3 3\n"
+                 "1 |3 3")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (forward-char 2) ; Position at column 2 (the '2').
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 2) ; Position at column 2 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-up 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest rectangle-last-column ()
+  "Check rectangle mode increments the last column of numbers."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "1 2 3\n"
+                 "1 2 3\n"
+                 "1 2 3"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "1 2 4\n"
+                 "1 2 4\n"
+                 "1 2 |4")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (forward-char 4) ; Position at column 4 (the '3').
+      (set-mark (point))
+      (goto-char (point-max))
+      (rectangle-mark-mode 1)
+      (shift-number-up 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Motion Tests
+
+(ert-deftest motion-enabled ()
+  "Check that shift-number-motion moves cursor and sets mark at number start."
+  (let ((text-initial "abc 123 def")
+        (text-expected "abc 124| def")
+        (mark-expected 5) ; Position of "124".
+        (shift-number-motion t))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string)))
+      (should (equal mark-expected (mark))))))
+
+(ert-deftest motion-enabled-down ()
+  "Check that shift-number-motion works with shift-number-down."
+  (let ((text-initial "abc 123 def")
+        (text-expected "abc 122| def")
+        (mark-expected 5) ; Position of "122".
+        (shift-number-motion t))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string)))
+      (should (equal mark-expected (mark))))))
+
+(ert-deftest motion-enabled-region ()
+  "Check that shift-number-motion works with region operations."
+  (let ((text-initial "1 2 3")
+        (text-expected "2 3 4|")
+        (mark-expected 1) ; Position of "2" (first number in region).
+        (shift-number-motion t))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string)))
+      (should (equal mark-expected (mark))))))
+
+(ert-deftest motion-disabled ()
+  "Check that cursor position is maintained and mark is not set when motion is 
disabled."
+  (let ((text-initial "123")
+        (text-expected "|124")
+        (shift-number-motion nil))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string)))
+      (should-not (mark)))))
+
+;; ---------------------------------------------------------------------------
+;; Multi-digit and Large Number Tests
+
+(ert-deftest large-number ()
+  "Check that large numbers are handled correctly."
+  (let ((text-initial "999999999")
+        (text-expected "|1000000000"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest large-negative ()
+  "Check that large negative numbers are handled correctly."
+  (let ((text-initial "-999999999")
+        (text-expected "|-1000000000"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Context Tests (number in various positions)
+
+(ert-deftest number-at-line-start ()
+  "Check number at the start of a line."
+  (let ((text-initial "42 is the answer")
+        (text-expected "|43 is the answer"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest number-at-line-end ()
+  "Check number at the end of a line."
+  (let ((text-initial "answer is 42")
+        (text-expected "|answer is 43"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest number-surrounded-by-parens ()
+  "Check number surrounded by parentheses."
+  (let ((text-initial "foo(42)")
+        (text-expected "foo(|43)"))
+    (with-shift-number-test text-initial
+      (forward-char 4) ; Position cursor on '4'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest number-in-array ()
+  "Check number in array-like syntax."
+  (let ((text-initial "[1, 2, 3]")
+        (text-expected "[|2, 2, 3]"))
+    (with-shift-number-test text-initial
+      (forward-char 1) ; Position cursor on '1'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest number-embedded-in-word ()
+  "Check number embedded within identifier-like text."
+  (let ((text-initial "var1name")
+        (text-expected "var|2name"))
+    (with-shift-number-test text-initial
+      (forward-char 3) ; Position cursor on '1'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Decimal Number Tests
+
+(ert-deftest decimal-integer-part ()
+  "Check that cursor on integer part of decimal increments integer."
+  (let ((text-initial "3.14")
+        (text-expected "|4.14"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest decimal-fractional-part ()
+  "Check that cursor on fractional part of decimal increments fraction."
+  (let ((text-initial "3.14")
+        (text-expected "3.|15"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '1'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest decimal-fractional-overflow ()
+  "Check that fractional part can overflow its width."
+  (let ((text-initial "3.99")
+        (text-expected "3.|100"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '9'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest decimal-cursor-on-point ()
+  "Check that cursor on decimal point finds the integer part."
+  (let ((text-initial "3.14")
+        (text-expected "4|.14"))
+    (with-shift-number-test text-initial
+      (forward-char 1) ; Position cursor on '.'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-mantissa ()
+  "Check that scientific notation mantissa is incremented independently."
+  (let ((text-initial "1e10")
+        (text-expected "|2e10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-exponent ()
+  "Check that scientific notation exponent is incremented independently."
+  (let ((text-initial "1e10")
+        (text-expected "1e|11"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '1' in exponent.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-uppercase ()
+  "Check that uppercase E in scientific notation works."
+  (let ((text-initial "1E10")
+        (text-expected "|2E10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-negative-exponent ()
+  "Check that negative exponent in scientific notation is handled."
+  (let ((text-initial "1e-10")
+        (text-expected "1e-|9"))
+    (with-shift-number-test text-initial
+      (forward-char 3) ; Position cursor on '1' in exponent.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-positive-exponent ()
+  "Check that explicit positive exponent in scientific notation is handled."
+  (let ((text-initial "1e+10")
+        (text-expected "1e+|11"))
+    (with-shift-number-test text-initial
+      (forward-char 3) ; Position cursor on '1' in exponent.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-cursor-on-exponent-sign ()
+  "Check that cursor on + in exponent finds the exponent."
+  (let ((text-initial "1e+10")
+        (text-expected "1e|+11"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '+'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-cursor-on-e ()
+  "Check that cursor on 'e' finds the mantissa."
+  (let ((text-initial "1e10")
+        (text-expected "2|e10"))
+    (with-shift-number-test text-initial
+      (forward-char 1) ; Position cursor on 'e'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest underscore-separator ()
+  "Check that underscore-separated numbers are treated as separate numbers."
+  (let ((text-initial "1_000")
+        (text-expected "|2_000"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hexadecimal-prefix ()
+  "Check that hexadecimal prefix is treated as separate number."
+  (let ((text-initial "0x10")
+        (text-expected "|1x10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest multiple-decimal-points ()
+  "Check behavior with multiple decimal points like version numbers."
+  (let ((text-initial "1.2.3")
+        (text-expected "|2.2.3"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest multiple-decimal-points-middle ()
+  "Check incrementing middle component of version number."
+  (let ((text-initial "1.2.3")
+        (text-expected "1.|3.3"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '2'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest multiple-decimal-points-last ()
+  "Check incrementing last component of version number."
+  (let ((text-initial "1.2.3")
+        (text-expected "1.2.|4"))
+    (with-shift-number-test text-initial
+      (forward-char 4) ; Position cursor on '3'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest all-zeros ()
+  "Check that all zeros increment correctly."
+  (let ((text-initial "000")
+        (text-expected "|001"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest all-zeros-to-negative ()
+  "Check that all zeros can decrement to negative with leading zeros."
+  (let ((text-initial "000")
+        (text-expected "|-001"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-decimal-mantissa ()
+  "Check scientific notation with decimal in mantissa."
+  (let ((text-initial "1.5e10")
+        (text-expected "|2.5e10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest scientific-notation-decimal-mantissa-fractional ()
+  "Check incrementing fractional part of decimal mantissa in scientific 
notation."
+  (let ((text-initial "1.5e10")
+        (text-expected "1.|6e10"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '5'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest tab-separated-numbers ()
+  "Check that tab-separated numbers work correctly."
+  (let ((text-initial "1\t2\t3")
+        (text-expected "1\t|3\t3"))
+    (with-shift-number-test text-initial
+      (forward-char 2) ; Position cursor on '2'.
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(provide 'shift-number-tests)
+;;; shift-number-tests.el ends here
diff --git a/tests/shift-number-tests.sh b/tests/shift-number-tests.sh
new file mode 100755
index 0000000000..fc0617e2ab
--- /dev/null
+++ b/tests/shift-number-tests.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+
+${EMACS:-emacs} \
+    -batch \
+    -l "$SCRIPT_PATH/shift-number-tests.el" \
+    -f ert-run-tests-batch-and-exit


Reply via email to