aboutsummaryrefslogtreecommitdiff
path: root/tests/test-duet-complexity.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-duet-complexity.el')
-rw-r--r--tests/test-duet-complexity.el126
1 files changed, 126 insertions, 0 deletions
diff --git a/tests/test-duet-complexity.el b/tests/test-duet-complexity.el
new file mode 100644
index 0000000..4929a74
--- /dev/null
+++ b/tests/test-duet-complexity.el
@@ -0,0 +1,126 @@
+;;; test-duet-complexity.el --- Tests for the duet complexity scanner -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; 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.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Unit tests for scripts/duet-complexity.el — the homegrown McCabe branch
+;; counter behind `make complexity'. The counting logic is pure (operates on
+;; already-read forms), so Normal/Boundary/Error cases need no I/O; only the
+;; file-scanning tests touch a temp file.
+
+;;; Code:
+
+(require 'ert)
+(require 'duet-complexity
+ (expand-file-name "../scripts/duet-complexity.el"
+ (file-name-directory (or load-file-name buffer-file-name))))
+
+;;; Normal cases — base count and single constructs
+
+(ert-deftest test-duet-complexity-empty-body-is-one ()
+ "A function with no decision points has complexity 1."
+ (should (= 1 (duet-complexity-of-form '(defun f ())))))
+
+(ert-deftest test-duet-complexity-single-if-is-two ()
+ "One `if' adds a single decision point."
+ (should (= 2 (duet-complexity-of-form '(defun f (x) (if x 1 2))))))
+
+(ert-deftest test-duet-complexity-when-unless-each-add-one ()
+ "`when' and `unless' each add one decision point."
+ (should (= 2 (duet-complexity-of-form '(defun f (x) (when x 1)))))
+ (should (= 2 (duet-complexity-of-form '(defun f (x) (unless x 1))))))
+
+(ert-deftest test-duet-complexity-loops-add-one ()
+ "Looping forms each add one decision point."
+ (should (= 2 (duet-complexity-of-form '(defun f (xs) (dolist (x xs) x)))))
+ (should (= 2 (duet-complexity-of-form '(defun f (n) (dotimes (i n) i)))))
+ (should (= 2 (duet-complexity-of-form '(defun f (x) (while x (setq x nil)))))))
+
+(ert-deftest test-duet-complexity-cond-one-per-clause ()
+ "`cond' adds one decision point per clause."
+ (should (= 4 (duet-complexity-of-form
+ '(defun f (x) (cond (a 1) (b 2) (t 3)))))))
+
+(ert-deftest test-duet-complexity-nested-decisions-accumulate ()
+ "A nested branch inside another counts in addition to the outer one."
+ (should (= 3 (duet-complexity-of-form
+ '(defun f (x) (when x (if x 1 2)))))))
+
+;;; Boundary cases — boolean operators, quoting, pattern matching
+
+(ert-deftest test-duet-complexity-and-adds-operands-minus-one ()
+ "`and' adds one decision point per short-circuit (operands minus one)."
+ (should (= 3 (duet-complexity-of-form '(defun f (a b c) (and a b c))))))
+
+(ert-deftest test-duet-complexity-or-two-operands-adds-one ()
+ "`or' with two operands adds a single decision point."
+ (should (= 2 (duet-complexity-of-form '(defun f (a b) (or a b))))))
+
+(ert-deftest test-duet-complexity-quoted-data-is-not-counted ()
+ "Branch-looking forms inside a quote are data, not control flow."
+ (should (= 1 (duet-complexity-of-form '(defun f () '(if a b c))))))
+
+(ert-deftest test-duet-complexity-pcase-counts-clauses-not-patterns ()
+ "`pcase' counts one per clause; an `or' pattern is not a boolean `or'."
+ (should (= 4 (duet-complexity-of-form
+ '(defun f (x) (pcase x ((or 1 2) 'a) (3 'b) (_ 'c)))))))
+
+(ert-deftest test-duet-complexity-condition-case-counts-handlers ()
+ "`condition-case' adds one decision point per handler."
+ (should (= 3 (duet-complexity-of-form
+ '(defun f () (condition-case err (foo) (error 1) (quit 2)))))))
+
+(ert-deftest test-duet-complexity-handles-defmacro-and-cl-defun ()
+ "Defun-like heads other than `defun' are measured the same way."
+ (should (= 2 (duet-complexity-of-form '(defmacro f (x) (if x 1 2)))))
+ (should (= 2 (duet-complexity-of-form '(cl-defun f (x) (if x 1 2))))))
+
+;;; File scanning
+
+(ert-deftest test-duet-complexity-scan-file-returns-name-score-pairs ()
+ "Scanning a file returns one (NAME . COMPLEXITY) pair per defun-like form."
+ (let ((file (make-temp-file "duet-cx" nil ".el"
+ "(defun simple () 1)\n(defun branchy (x) (if x (when x 1) 2))\n")))
+ (unwind-protect
+ (let ((results (duet-complexity-scan-file file)))
+ (should (equal 1 (cdr (assq 'simple results))))
+ (should (equal 3 (cdr (assq 'branchy results))))
+ (should (= 2 (length results))))
+ (delete-file file))))
+
+(ert-deftest test-duet-complexity-scan-file-ignores-non-defuns ()
+ "Top-level forms that are not defun-like are skipped by the scanner."
+ (let ((file (make-temp-file "duet-cx" nil ".el"
+ "(defvar x 1)\n(require 'foo)\n(defun real (y) (if y 1 2))\n")))
+ (unwind-protect
+ (let ((results (duet-complexity-scan-file file)))
+ (should (= 1 (length results)))
+ (should (equal 2 (cdr (assq 'real results)))))
+ (delete-file file))))
+
+;;; Threshold gate
+
+(ert-deftest test-duet-complexity-over-threshold-filters ()
+ "Only functions above the threshold are returned."
+ (let ((results '((a . 3) (b . 11) (c . 10) (d . 15))))
+ (should (equal '((b . 11) (d . 15))
+ (duet-complexity-over-threshold results 10)))))
+
+(provide 'test-duet-complexity)
+;;; test-duet-complexity.el ends here