aboutsummaryrefslogtreecommitdiff
path: root/tests/test-org-drill-statistics-render-trends.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-31 08:35:16 -0500
committerCraig Jennings <c@cjennings.net>2026-05-31 08:35:16 -0500
commit26cc4472dea261a1ad13fbee8fb6a91b019f77bb (patch)
treea7063337b05c3ea278a5b910d0f1420de033dfe8 /tests/test-org-drill-statistics-render-trends.el
parent532ce532465834ce06238648ba1490c48bed29ca (diff)
downloadorg-drill-26cc4472dea261a1ad13fbee8fb6a91b019f77bb.tar.gz
org-drill-26cc4472dea261a1ad13fbee8fb6a91b019f77bb.zip
feat: add the org-drill statistics dashboard renderer
Step 1 shipped the session-log data layer. This is the renderer on top of it. org-drill-statistics opens a read-only dashboard with five sections: an overview (card counts plus a last-session recap), trends (reviews-per-day and pass-rate-per-day quadrant-block sparklines over the trend window, plus a 12-week table), a quality histogram, a needs-attention view (leech candidates, long-overdue, and forgotten-new cards), and a 7-day forecast counted from SCHEDULED dates. A buffer-wide filter (scope, range, algorithm) sits in the header and cycles with s/r/a. The other keys are q to bury, g to refresh, e for the CSV-export hook that lands next, and RET to follow the card link at point. The aggregation math lives in pure helpers (day-bucketing, sparkline scaling, weekly aggregates, the histogram, the attention selectors, forecast bucketing). The render helpers are thin string formatters over them, so the logic is unit-tested independently of the UI. New defcustoms tune the views: org-drill-statistics-trend-days, -forecast-days, -attention-row-limit, and -leech-quality-threshold. I added require 'calendar for the Monday week-start arithmetic in the weekly aggregates. CSV export and the manual and README entries are the step-3 follow-on.
Diffstat (limited to 'tests/test-org-drill-statistics-render-trends.el')
-rw-r--r--tests/test-org-drill-statistics-render-trends.el129
1 files changed, 129 insertions, 0 deletions
diff --git a/tests/test-org-drill-statistics-render-trends.el b/tests/test-org-drill-statistics-render-trends.el
new file mode 100644
index 0000000..bf50b83
--- /dev/null
+++ b/tests/test-org-drill-statistics-render-trends.el
@@ -0,0 +1,129 @@
+;;; test-org-drill-statistics-render-trends.el --- Tests for render-trends statistics -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; ERT tests for the org-drill statistics dashboard render-trends block.
+
+;;; Code:
+
+(require 'ert)
+(require 'org-drill)
+(require 'cl-lib)
+(require 'org)
+
+;;; Tests for the Trends render helper (render 2/5).
+
+
+(defun org-drill-statistics-test--record (start-day-offset qualities
+ &optional duration-min)
+ "Build an `org-drill-session-record' for trends tests.
+START-DAY-OFFSET is an integer day offset from today (0 is today, -7 is
+a week ago). QUALITIES is a list of int qualities, stored as a vector.
+DURATION-MIN defaults to 10 minutes. The record's start-time and
+end-time floats land on the requested day; only the day component is
+load bearing for these tests."
+ (let* ((duration (or duration-min 10))
+ (today-secs (float-time (current-time)))
+ (start (+ today-secs (* start-day-offset 86400.0)))
+ (end (+ start (* duration 60.0))))
+ (make-org-drill-session-record
+ :start-time start
+ :end-time end
+ :scope 'file
+ :algorithm 'sm5
+ :qualities (vconcat qualities)
+ :pass-percent (org-drill--compute-pass-percent (vconcat qualities))
+ :new-count 0
+ :mature-count (length qualities)
+ :failed-count 0
+ :cram-mode nil)))
+
+(ert-deftest test-org-drill-statistics-render-trends-has-subheading ()
+ "The rendered section opens with the \"* Trends\" org subheading."
+ (let ((out (org-drill-statistics--render-trends nil)))
+ (should (string-prefix-p "* Trends\n" out))))
+
+(ert-deftest test-org-drill-statistics-render-trends-empty-log ()
+ "An empty log still renders both sparkline lines and a table header.
+The sparklines are all-space (no data) and the table has only its header
+and separator rows."
+ (let ((out (org-drill-statistics--render-trends nil)))
+ (should (string-match-p "Reviews/day (last 90):" out))
+ (should (string-match-p "Pass rate/day (last 90):" out))
+ (should (string-match-p "| Week | Reviews | Pass % | Avg min |" out))
+ (should (string-match-p "|------" out))))
+
+(ert-deftest test-org-drill-statistics-render-trends-sparkline-glyph ()
+ "A record today puts a non-space block glyph in the reviews sparkline.
+The final sparkline column (today) must be a quadrant block, not the
+space rendered for empty days."
+ (let* ((log (list (org-drill-statistics-test--record 0 '(5 4 3))))
+ (out (org-drill-statistics--render-trends log))
+ (line (car (seq-filter
+ (lambda (l) (string-prefix-p "Reviews/day" l))
+ (split-string out "\n")))))
+ ;; The sparkline glyph for the busiest day is the full block, since
+ ;; today is the only day with data so it scales to the ceiling.
+ (should (string-match-p "█" line))))
+
+(ert-deftest test-org-drill-statistics-render-trends-weekly-row ()
+ "A this-week session produces a body row with its counts.
+The row carries the Monday date of this week, the review count, the
+pass percentage, and a one-decimal average duration."
+ (let* ((today (time-to-days (current-time)))
+ (week-start (org-drill-statistics--week-start-day today))
+ (expected-date (org-drill-statistics--format-week-start week-start))
+ ;; Three qualities, two passes (> failure-quality default 2).
+ (log (list (org-drill-statistics-test--record 0 '(5 4 1) 20)))
+ (out (org-drill-statistics--render-trends log)))
+ ;; Pass percent: 2 of 3 -> 67. Avg duration: 20.0 minutes.
+ (should (string-match-p
+ (regexp-quote (format "| %s | 3 | 67 | 20.0 |" expected-date))
+ out))))
+
+(ert-deftest test-org-drill-statistics-render-trends-twelve-week-rows ()
+ "The table body has exactly 12 week rows, one per week in the window."
+ (let* ((out (org-drill-statistics--render-trends nil))
+ (lines (split-string out "\n" t))
+ (body (seq-filter
+ (lambda (l)
+ (and (string-prefix-p "| " l)
+ (not (string-match-p "Week" l))))
+ lines)))
+ (should (= (length body) 12))))
+
+(ert-deftest test-org-drill-statistics-render-trends-algorithm-filter ()
+ "Passing an algorithm filters records out of the aggregates.
+A record under `sm5' is excluded when the section filters for `sm2',
+leaving an empty (all-zero) this-week row."
+ (let* ((today (time-to-days (current-time)))
+ (week-start (org-drill-statistics--week-start-day today))
+ (date (org-drill-statistics--format-week-start week-start))
+ (log (list (org-drill-statistics-test--record 0 '(5 4 3) 15)))
+ (out (org-drill-statistics--render-trends log 'sm2)))
+ ;; sm5 record is filtered out, so this week's row is zeroed.
+ (should (string-match-p
+ (regexp-quote (format "| %s | 0 | 0 | 0.0 |" date))
+ out))
+ ;; And the unfiltered render keeps it.
+ (let ((unfiltered (org-drill-statistics--render-trends log)))
+ (should (string-match-p
+ (regexp-quote (format "| %s | 3 |" date))
+ unfiltered)))))
+
+(ert-deftest test-org-drill-statistics-render-trends-pass-rate-absolute-scale ()
+ "The pass-rate sparkline scales against 100, not the window peak.
+A day with a 50 percent pass rate must render a mid-height glyph, not
+the full block it would reach if scaled to its own maximum."
+ (let* ((log (list (org-drill-statistics-test--record 0 '(5 1))))
+ (out (org-drill-statistics--render-trends log))
+ (line (car (seq-filter
+ (lambda (l) (string-prefix-p "Pass rate/day" l))
+ (split-string out "\n")))))
+ ;; 1 pass of 2 -> 50 percent. Scaled to 100 over an 8-glyph charset,
+ ;; round(50/100 * 7) = 4 -> the 5th glyph, not the full block.
+ (should-not (string-match-p "█" line))
+ (should (string-match-p "▅" line))))
+
+(provide 'test-org-drill-statistics-render-trends)
+
+;;; test-org-drill-statistics-render-trends.el ends here