Subject: Re: bug#70939: [PATCH] Add commands to run unit tests in go-ts-mode

From: Ankit Gadiya <[email-masked]>
Date: Sat, 18 May 2024 15:24:14 +0530

> It's okay to support buffer-local values, but using the value of the
> variable will do that automatically; you don't have to verify it has a
> local binding.  What I don't understand is why does your code _reject_
> global bindings.  If some user wants to set up a global value for some
> reason, or have a default global value and special local values in
> some cases, why should we prevent that?

You are absolutely right, now I realize that local-variable-p is overly
conservative. Since I didn't define the variable in the first place I had to do
the check and even then boundp would have been better.

> > But now I'm thinking maybe it makes sense to add it as defvar-local under the
> > go-ts-mode? Please guide me with what will be the correct way to implement it.
>
> Yes, defvar-local could be what we want.  But I first would like to
> understand what is special about this variable that it should be
> buffer-local.

With the improved understanding, I believe defcustom would be better for the
variable. Then I don't even need the the boundp check since the variable is
always present and it can be replaced by just the non-nil check instead.

As an improvement, I'm also changing the variable-type to a list of strings
instead of a single comma separated string.

I'm submitting an updated Patch with the following changes:
* Add commit log
* Fix the text formatting.
* Update the keybindings to use the C-c C-t prefix.
* Improve regexp matching to be more strict.
* Define =go-ts-mode-build-tags= variable in the module and use it in the test
  functions.

-- 
Ankit
From cc6b3267e350861271e366bb3a6c2891cfa8557e Mon Sep 17 00:00:00 2001
From: Ankit R Gadiya <[email-masked]>
Date: Tue, 14 May 2024 00:14:03 +0530
Subject: [PATCH] Add commands to run unit tests in go-ts-mode

Two new commands are added in the go-ts-mode to run unit tests.
The go-ts-mode-test-function-at-point command runs the current
test function at point or all the functions in the active region
when region is active.  It is bound to "C-c C-t t". The
go-ts-mode-test-package command runs all tests in the current
package.  It is bound to "C-c C-t p".

* lisp/progmodes/go-ts-mode.el (go-ts-mode-build-tags): New variable.
(go-ts-mode-map): New map variable.
(go-ts-mode--get-build-tags-flag): New function.
(go-ts-mode--compile-test): New function.
(go-ts-mode--find-defun-at): New function.
(go-ts-mode--get-function-regexp): New function.
(go-ts-mode--get-functions-in-range): New function.
(go-ts-mode--get-test-regexp-at-point): New function.
(go-ts-mode-test-function-at-point): New function.
(go-ts-mode-test-package): New function.
* etc/NEWS: Mention the change.

(Bug#70939)
---
 etc/NEWS                     | 13 +++++++
 lisp/progmodes/go-ts-mode.el | 73 +++++++++++++++++++++++++++++++++++-
 2 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/etc/NEWS b/etc/NEWS
index 77b2749fe43..c978c793503 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1236,6 +1236,19 @@ This command adds a docstring comment to the current defun.  If a
 comment already exists, point is only moved to the comment.  It is
 bound to 'C-c C-d' in 'go-ts-mode'.
 
+*** New unit test commands.
+Two new commands are now available to run unit tests.
+
+The 'go-ts-mode-test-function-at-point' command runs the unit test at
+point.  If a region is active, it runs all the unit tests under the
+region.  It is bound to 'C-c C-t' in 'go-ts-mode'.
+
+The 'go-ts-mode-test-package' command runs all unit tests under the
+package of the current buffer.  It is bound to 'C-c C-p' in 'go-ts-mode'.
+
+The 'go-ts-mode-build-tags' variable is available to set a list of build
+tags for the test commands.
+
 ** Man mode
 
 +++
diff --git a/lisp/progmodes/go-ts-mode.el b/lisp/progmodes/go-ts-mode.el
index aef224ab3fa..f6d872b5210 100644
--- a/lisp/progmodes/go-ts-mode.el
+++ b/lisp/progmodes/go-ts-mode.el
@@ -46,6 +46,12 @@ go-ts-mode-indent-offset
   :safe 'integerp
   :group 'go)
 
+(defcustom go-ts-mode-build-tags nil
+  "List of go build tags for the test commands."
+  :version "30.1"
+  :type '(repeat string)
+  :group 'go)
+
 (defvar go-ts-mode--syntax-table
   (let ((table (make-syntax-table)))
     (modify-syntax-entry ?+   "."      table)
@@ -237,7 +243,9 @@ go-ts-mode--font-lock-settings
 (defvar-keymap go-ts-mode-map
   :doc "Keymap used in Go mode, powered by tree-sitter"
   :parent prog-mode-map
-  "C-c C-d" #'go-ts-mode-docstring)
+  "C-c C-d" #'go-ts-mode-docstring
+  "C-c C-t t" #'go-ts-mode-test-function-at-point
+  "C-c C-t p" #'go-ts-mode-test-package)
 
 ;;;###autoload
 (define-derived-mode go-ts-mode prog-mode "Go"
@@ -370,6 +378,69 @@ go-ts-mode--comment-on-previous-line-p
      (<= (treesit-node-start node) point (treesit-node-end node))
      (string-equal "comment" (treesit-node-type node)))))
 
+(defun go-ts-mode--get-build-tags-flag ()
+  "Return compile flag for build tags.
+This function respects the `go-ts-mode-build-tags' variable for
+specifying build tags."
+  (if go-ts-mode-build-tags
+      (format "-tags %s" (string-join go-ts-mode-build-tags ","))
+    ""))
+
+(defun go-ts-mode--compile-test (regexp)
+  "Compile the tests matching REGEXP.
+This function respects `go-ts-mode-build-tags' variable for specifying
+build tags."
+  (compile (format "go test -v %s -run '%s'"
+                   (go-ts-mode--get-build-tags-flag)
+                   regexp)))
+
+(defun go-ts-mode--find-defun-at (start)
+  "Return the first defun node from START."
+  (let ((thing (or treesit-defun-type-regexp 'defun)))
+    (or (treesit-thing-at start thing)
+        (treesit-thing-next start thing))))
+
+(defun go-ts-mode--get-function-regexp (name)
+  (format "^%s$" name))
+
+(defun go-ts-mode--get-functions-in-range (start end)
+  "Return a list with names of all defuns in the range."
+  (let* ((node (go-ts-mode--find-defun-at start))
+	 (name (treesit-defun-name node))
+         (node-start (treesit-node-start node))
+         (node-end (treesit-node-end node)))
+    (if (or (not node)
+            (> start node-end)
+	    (< end node-start))
+	nil
+      (cons (go-ts-mode--get-function-regexp name)
+            (go-ts-mode--get-functions-in-range (treesit-node-end node) end)))))
+
+(defun go-ts-mode--get-test-regexp-at-point ()
+  "Return a regular expression for tests at point.
+If region is active, the regexp will include all the functions under the
+region."
+  (if (region-active-p)
+      (string-join (go-ts-mode--get-functions-in-range (region-beginning)
+                                                       (region-end))
+                   "|")
+    (go-ts-mode--get-function-regexp (treesit-defun-name (treesit-defun-at-point)))))
+
+(defun go-ts-mode-test-function-at-point ()
+  "Run the unit test at point.
+If the point is anywhere in the test function, that function will be
+run.  If the region is selected, all the functions under the region will
+be run."
+    (interactive)
+    (go-ts-mode--compile-test (go-ts-mode--get-test-regexp-at-point)))
+
+(defun go-ts-mode-test-package ()
+  "Run all the unit tests under the current package."
+  (interactive)
+  (compile (format "go test -v %s -run %s"
+                   (go-ts-mode--get-build-tags-flag)
+                   default-directory)))
+
 ;; go.mod support.
 
 (defvar go-mod-ts-mode--syntax-table
-- 
2.39.2