branch: elpa/flamegraph
commit 824d9ac1216af26a86f433decdc70e606a62b56e
Author: Dmitry Gutov <[email protected]>
Commit: Dmitry Gutov <[email protected]>

    Add tests
    
    Frame parsing
    Predicates
    Enclosing form
    Walker
    Symbol location
    Snippet end-to-end
---
 test/flamegraph-tests.el | 246 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 246 insertions(+)

diff --git a/test/flamegraph-tests.el b/test/flamegraph-tests.el
new file mode 100644
index 0000000000..ad63da6c88
--- /dev/null
+++ b/test/flamegraph-tests.el
@@ -0,0 +1,246 @@
+;;; flamegraph-tests.el --- Tests for flamegraph.el  -*- lexical-binding: t; 
-*-
+
+;; Copyright (C) 2026 Dmitry Gutov
+
+;; Author: Dmitry Gutov <[email protected]>
+
+;; This file 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.
+
+;;; Commentary:
+
+;; Run with:
+;;   emacs -Q -batch -L . -l test/flamegraph-tests.el \
+;;     -f ert-run-tests-batch-and-exit
+
+;;; Code:
+
+(require 'ert)
+(require 'profiler)
+(require 'flamegraph)
+
+;;; Frame name + embedded source location parsing
+
+(ert-deftest flamegraph-test-entry-location-perf ()
+  "FUNC:/abs/path:LINE form from \"perf script -F +srcline\" folded stacks."
+  (should (equal (flamegraph--entry-location
+                  "redisplay_window:/path/to/emacs/src/xdisp.c:21020")
+                 (cons "/path/to/emacs/src/xdisp.c" 21020))))
+
+(ert-deftest flamegraph-test-entry-location-pyspy ()
+  "NAME (FILE:LINE) form, as py-spy emits."
+  (should (equal (flamegraph--entry-location "work (app/main.py:20)")
+                 (cons "app/main.py" 20))))
+
+(ert-deftest flamegraph-test-entry-location-rbspy ()
+  "NAME - FILE:LINE form, as rbspy emits."
+  (should (equal (flamegraph--entry-location "block in foo - lib/foo.rb:12")
+                 (cons "lib/foo.rb" 12))))
+
+(ert-deftest flamegraph-test-entry-location-none ()
+  "Strings without an embedded location return nil."
+  (should (null (flamegraph--entry-location "emacs")))
+  (should (null (flamegraph--entry-location "[unknown]")))
+  (should (null (flamegraph--entry-location ""))))
+
+;;; Display name strips the embedded location
+
+(ert-deftest flamegraph-test-frame-display-name-strips-location ()
+  (should (equal (flamegraph--frame-display-name
+                  "redisplay_window:/path/to/emacs/src/xdisp.c:21020")
+                 "redisplay_window"))
+  (should (equal (flamegraph--frame-display-name "work (app/main.py:20)")
+                 "work"))
+  (should (equal (flamegraph--frame-display-name "block in foo - 
lib/foo.rb:12")
+                 "block in foo")))
+
+(ert-deftest flamegraph-test-frame-display-name-pass-through ()
+  (should (equal (flamegraph--frame-display-name "emacs") "emacs"))
+  (should (equal (flamegraph--frame-display-name "[unknown]") "[unknown]")))
+
+;;; skip-through-p / callee-name-acceptable-p
+
+(ert-deftest flamegraph-test-skip-through-p ()
+  (should (flamegraph--skip-through-p 'if))
+  (should (flamegraph--skip-through-p 'let))
+  (should (flamegraph--skip-through-p 'progn))
+  (should (flamegraph--skip-through-p 'condition-case))
+  (should-not (flamegraph--skip-through-p 'mapcar))
+  (should-not (flamegraph--skip-through-p 'flamegraph-describe))
+  (should-not (flamegraph--skip-through-p "string-entry"))
+  (should-not (flamegraph--skip-through-p nil)))
+
+(ert-deftest flamegraph-test-callee-name-acceptable-p ()
+  (should (flamegraph--callee-name-acceptable-p "foo"))
+  (should (flamegraph--callee-name-acceptable-p "foo-bar"))
+  (should (flamegraph--callee-name-acceptable-p
+           "Fnext_single_char_property_change"))
+  (should-not (flamegraph--callee-name-acceptable-p ""))
+  (should-not (flamegraph--callee-name-acceptable-p "[unknown]"))
+  (should-not (flamegraph--callee-name-acceptable-p "foo bar"))
+  (should-not (flamegraph--callee-name-acceptable-p "(foo)")))
+
+;;; Enclosing form region
+
+(ert-deftest flamegraph-test-enclosing-form-region ()
+  "From a position on `bar', the enclosing form is `(bar (baz))'."
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert "(foo (bar (baz)))")
+    (goto-char (point-min))
+    (re-search-forward "\\_<bar\\_>")
+    (let ((r (flamegraph--enclosing-form-region (match-beginning 0))))
+      (should r)
+      (should (equal (buffer-substring-no-properties (car r) (cdr r))
+                     "(bar (baz))")))))
+
+;;; Walker — `flamegraph--collect-nested-call-sites'
+
+(defmacro flamegraph-test--with-elisp-source (src &rest body)
+  "Run BODY in a temp buffer holding SRC in `emacs-lisp-mode'."
+  (declare (indent 1))
+  `(with-temp-buffer
+     (emacs-lisp-mode)
+     (insert ,src)
+     (goto-char (point-min))
+     ,@body))
+
+(defun flamegraph-test--region-texts (regions)
+  "Texts spanned by REGIONS (a list of (BEG . END)) in current buffer."
+  (mapcar (lambda (r) (buffer-substring-no-properties (car r) (cdr r)))
+          regions))
+
+(ert-deftest flamegraph-test-walker-strict-nesting ()
+  "Real-function chains narrow strictly: a stray sibling is not matched.
+For `(foo (bar (baz)))' with a separate `(bar 'unrelated)' elsewhere,
+only the structurally-nested foo/bar/baz should be returned — the
+stray `bar' is outside the foo-form's region."
+  (flamegraph-test--with-elisp-source
+      "(defun outer ()
+  (foo (bar (baz)))
+  (bar 'unrelated))"
+    (let* ((baz (profiler-make-calltree :entry 'baz :count 1))
+           (bar (profiler-make-calltree :entry 'bar :count 1 :children (list 
baz)))
+           (foo (profiler-make-calltree :entry 'foo :count 1 :children (list 
bar)))
+           (outer (profiler-make-calltree :entry 'outer :count 2
+                                          :children (list foo)))
+           (regions (flamegraph--collect-nested-call-sites
+                     outer (point-min) (point-max))))
+      (should (equal (flamegraph-test--region-texts regions)
+                     '("foo" "bar" "baz"))))))
+
+(ert-deftest flamegraph-test-walker-macro-expansion-transparent ()
+  "Skip-through frames absent from source must not stop the descent.
+The profiler records post-expansion frames (here `while'/`let' from
+`dolist'); the walker should treat them as transparent and still find
+`company--perform' in the source."
+  (flamegraph-test--with-elisp-source
+      "(defun outer (xs)
+  (dolist (x xs)
+    (company--perform x)))"
+    (let* ((cp (profiler-make-calltree :entry 'company--perform :count 100))
+           (let-n (profiler-make-calltree :entry 'let :count 100
+                                          :children (list cp)))
+           (while-n (profiler-make-calltree :entry 'while :count 100
+                                            :children (list let-n)))
+           (outer (profiler-make-calltree :entry 'outer :count 100
+                                          :children (list while-n)))
+           (regions (flamegraph--collect-nested-call-sites
+                     outer (point-min) (point-max))))
+      (should (equal (flamegraph-test--region-texts regions)
+                     '("company--perform"))))))
+
+(ert-deftest flamegraph-test-walker-no-special-form-anchoring ()
+  "Special forms must never anchor the search.
+A literal `if' in source unrelated to the profile's top-level `if'
+(typical with `when'/`unless' macro expansion) should not trap the
+walker into searching only inside that literal `if' form — the real
+callees outside it must still be found."
+  (flamegraph-test--with-elisp-source
+      "(defun caller ()
+  (when test                                ; profile records (if test ...)
+    (company-abort))
+  (if cond                                  ; literal `if', unrelated
+      (company-call-frontends 'cmd)
+    (run-with-timer 1 nil 'k)))"
+    (let* ((cabort (profiler-make-calltree :entry 'company-abort :count 1))
+           (ccf    (profiler-make-calltree :entry 'company-call-frontends
+                                           :count 1))
+           (rwt    (profiler-make-calltree :entry 'run-with-timer :count 1))
+           (deep   (profiler-make-calltree :entry 'progn :count 3
+                                           :children (list cabort ccf rwt)))
+           (top-if (profiler-make-calltree :entry 'if :count 3
+                                           :children (list deep)))
+           (caller (profiler-make-calltree :entry 'caller :count 3
+                                           :children (list top-if)))
+           (regions (flamegraph--collect-nested-call-sites
+                     caller (point-min) (point-max))))
+      (should (equal (sort (flamegraph-test--region-texts regions) #'string<)
+                     '("company-abort" "company-call-frontends"
+                       "run-with-timer"))))))
+
+(ert-deftest flamegraph-test-walker-unsearchable-names-skipped ()
+  "An unsearchable name (e.g. \"[unknown]\") must not error or recurse."
+  (flamegraph-test--with-elisp-source
+      "(defun outer () (foo))"
+    (let* ((foo (profiler-make-calltree :entry 'foo :count 1))
+           (unk (profiler-make-calltree :entry "[unknown]" :count 1))
+           (outer (profiler-make-calltree :entry 'outer :count 2
+                                          :children (list unk foo)))
+           (regions (flamegraph--collect-nested-call-sites
+                     outer (point-min) (point-max))))
+      (should (equal (flamegraph-test--region-texts regions) '("foo"))))))
+
+;;; `flamegraph--symbol-location'
+
+(ert-deftest flamegraph-test-symbol-location ()
+  "Find the source file and line of a real Elisp function."
+  (let ((loc (flamegraph--symbol-location 'flamegraph-describe)))
+    (should loc)
+    (should (stringp (car loc)))
+    (should (string-suffix-p "flamegraph.el" (car loc)))
+    (should (integerp (cdr loc)))
+    (should (> (cdr loc) 0))))
+
+(ert-deftest flamegraph-test-symbol-location-unbound ()
+  (should (null (flamegraph--symbol-location
+                 'flamegraph-test--does-not-exist))))
+
+;;; `flamegraph--source-snippet' (end-to-end)
+
+(defmacro flamegraph-test--with-temp-source (var content &rest body)
+  "Bind VAR to a temp .el file containing CONTENT, run BODY, then delete."
+  (declare (indent 2))
+  `(let ((,var (make-temp-file "flamegraph-snip-" nil ".el" ,content)))
+     (unwind-protect (progn ,@body)
+       (delete-file ,var))))
+
+(ert-deftest flamegraph-test-source-snippet-marks-sampled-line ()
+  "The sampled line carries the ▸ marker."
+  (flamegraph-test--with-temp-source path
+      "(defun outer ()\n  (foo (bar))\n  (baz))\n"
+    (let ((s (flamegraph--source-snippet path 2 nil)))
+      (should s)
+      (should (string-match-p "^ ▸ +2 " s)))))
+
+(ert-deftest flamegraph-test-source-snippet-applies-highlight-face ()
+  "Callee matches in the snippet carry the `flamegraph-call-site' face."
+  (flamegraph-test--with-temp-source path
+      "(defun outer ()\n  (foo (bar)))\n"
+    (let* ((bar (profiler-make-calltree :entry 'bar :count 1))
+           (foo (profiler-make-calltree :entry 'foo :count 1
+                                        :children (list bar)))
+           (outer (profiler-make-calltree :entry 'outer :count 1
+                                          :children (list foo)))
+           (s (flamegraph--source-snippet
+               path 1
+               (lambda (b e)
+                 (flamegraph--collect-nested-call-sites outer b e)))))
+      (should s)
+      (should (text-property-any 0 (length s)
+                                 'face 'flamegraph-call-site s)))))
+
+(provide 'flamegraph-tests)
+;;; flamegraph-tests.el ends here

Reply via email to