;;; test-duet-complexity.el --- Tests for the duet complexity scanner -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; 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