aboutsummaryrefslogtreecommitdiff
path: root/tests/test-org-drill-statistics-weekly-aggregates.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-weekly-aggregates.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-weekly-aggregates.el')
-rw-r--r--tests/test-org-drill-statistics-weekly-aggregates.el184
1 files changed, 184 insertions, 0 deletions
diff --git a/tests/test-org-drill-statistics-weekly-aggregates.el b/tests/test-org-drill-statistics-weekly-aggregates.el
new file mode 100644
index 0000000..446ba54
--- /dev/null
+++ b/tests/test-org-drill-statistics-weekly-aggregates.el
@@ -0,0 +1,184 @@
+;;; test-org-drill-statistics-weekly-aggregates.el --- Tests for weekly-aggregates statistics -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; ERT tests for the org-drill statistics dashboard weekly-aggregates block.
+
+;;; Code:
+
+(require 'ert)
+(require 'org-drill)
+(require 'cl-lib)
+(require 'org)
+
+;;; Tests for org-drill-statistics--weekly-aggregates.
+;;
+;; All tests redefine `org-drill-statistics--today-day' to a fixed
+;; absolute day so the window is deterministic and never anchored to the
+;; real clock. The chosen day, 739767, is Sunday 2026-05-31; its
+;; Monday-based week start is 739761. Fixtures build records at noon of
+;; a chosen absolute day, which round-trips cleanly through the
+;; `time-to-days' path that `org-drill-statistics--record-day' uses.
+
+(defun test-org-drill-statistics-weekly--abs-to-float (abs hour)
+ "Return a float-time for HOUR (local) on absolute day ABS."
+ (let ((g (calendar-gregorian-from-absolute abs)))
+ (float-time
+ (encode-time (list 0 0 hour
+ (calendar-extract-day g)
+ (calendar-extract-month g)
+ (calendar-extract-year g)
+ nil -1 nil)))))
+
+(defun test-org-drill-statistics-weekly--rec (abs qualities &optional dur-min)
+ "Build a record starting at noon on ABS, lasting DUR-MIN minutes.
+QUALITIES is a sequence of integers; DUR-MIN defaults to 10."
+ (let ((start (test-org-drill-statistics-weekly--abs-to-float abs 12)))
+ (make-org-drill-session-record
+ :start-time start
+ :end-time (+ start (* 60 (or dur-min 10)))
+ :qualities (vconcat qualities)
+ :algorithm 'sm5)))
+
+(defmacro test-org-drill-statistics-weekly--with-today (abs &rest body)
+ "Run BODY with `org-drill-statistics--today-day' fixed to ABS."
+ (declare (indent 1))
+ `(cl-letf (((symbol-function 'org-drill-statistics--today-day)
+ (lambda () ,abs)))
+ ,@body))
+
+(defconst test-org-drill-statistics-weekly--today 739767
+ "Fixed today for tests: Sunday 2026-05-31, absolute day number.")
+(defconst test-org-drill-statistics-weekly--this-mon 739761
+ "Monday starting the week of `test-org-drill-statistics-weekly--today'.")
+
+;;; ---- Normal cases ----
+
+(ert-deftest test-org-drill-statistics-weekly-default-span ()
+ "Default WEEKS is 12, oldest-first, with each week 7 days apart."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let ((agg (org-drill-statistics--weekly-aggregates nil)))
+ (should (= 12 (length agg)))
+ (should (= (- test-org-drill-statistics-weekly--this-mon (* 7 11))
+ (plist-get (car agg) :week-start)))
+ (should (= test-org-drill-statistics-weekly--this-mon
+ (plist-get (car (last agg)) :week-start)))
+ (cl-loop for (a b) on agg while b
+ do (should (= 7 (- (plist-get b :week-start)
+ (plist-get a :week-start))))))))
+
+(ert-deftest test-org-drill-statistics-weekly-pooled-pass-percent ()
+ "Reviews sum pooled qualities; pass-percent is pooled, not averaged."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ ;; Two sessions this week: pooled (5 4 1 2) -> 2 pass of 4 -> 50.
+ (let* ((log (list (test-org-drill-statistics-weekly--rec
+ test-org-drill-statistics-weekly--today '(5 4) 10)
+ (test-org-drill-statistics-weekly--rec
+ (1+ test-org-drill-statistics-weekly--this-mon)
+ '(1 2) 30)))
+ (agg (org-drill-statistics--weekly-aggregates log))
+ (this (car (last agg))))
+ (should (= test-org-drill-statistics-weekly--this-mon
+ (plist-get this :week-start)))
+ (should (= 4 (plist-get this :reviews)))
+ (should (= 50 (plist-get this :pass-percent)))
+ (should (= 20.0 (plist-get this :avg-duration-min))))))
+
+(ert-deftest test-org-drill-statistics-weekly-records-spread-weeks ()
+ "Records land in their own Monday-based week buckets."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let* ((mon test-org-drill-statistics-weekly--this-mon)
+ (log (list (test-org-drill-statistics-weekly--rec
+ test-org-drill-statistics-weekly--today '(5))
+ (test-org-drill-statistics-weekly--rec
+ (- mon 7) '(0 0))
+ (test-org-drill-statistics-weekly--rec
+ (- mon 14) '(4))))
+ (agg (org-drill-statistics--weekly-aggregates log 12))
+ (by-start (mapcar (lambda (p) (cons (plist-get p :week-start)
+ (plist-get p :reviews)))
+ agg)))
+ (should (= 1 (cdr (assoc mon by-start))))
+ (should (= 2 (cdr (assoc (- mon 7) by-start))))
+ (should (= 1 (cdr (assoc (- mon 14) by-start)))))))
+
+;;; ---- Boundary cases ----
+
+(ert-deftest test-org-drill-statistics-weekly-empty-log ()
+ "An empty log yields WEEKS all-zero plists."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let ((agg (org-drill-statistics--weekly-aggregates nil 3)))
+ (should (= 3 (length agg)))
+ (dolist (p agg)
+ (should (= 0 (plist-get p :reviews)))
+ (should (= 0 (plist-get p :pass-percent)))
+ (should (= 0.0 (plist-get p :avg-duration-min)))))))
+
+(ert-deftest test-org-drill-statistics-weekly-single-week ()
+ "WEEKS = 1 keeps only the current week's records."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let* ((log (list (test-org-drill-statistics-weekly--rec
+ test-org-drill-statistics-weekly--today '(5 5))
+ (test-org-drill-statistics-weekly--rec
+ (- test-org-drill-statistics-weekly--this-mon 7) '(0))))
+ (agg (org-drill-statistics--weekly-aggregates log 1)))
+ (should (= 1 (length agg)))
+ (should (= test-org-drill-statistics-weekly--this-mon
+ (plist-get (car agg) :week-start)))
+ (should (= 2 (plist-get (car agg) :reviews)))
+ (should (= 100 (plist-get (car agg) :pass-percent))))))
+
+(ert-deftest test-org-drill-statistics-weekly-out-of-window-dropped ()
+ "Records older than the window are not bucketed."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let* ((log (list (test-org-drill-statistics-weekly--rec
+ (- test-org-drill-statistics-weekly--this-mon (* 7 5))
+ '(5))))
+ (agg (org-drill-statistics--weekly-aggregates log 3)))
+ (should (cl-every (lambda (p) (= 0 (plist-get p :reviews))) agg)))))
+
+(ert-deftest test-org-drill-statistics-weekly-week-boundary-monday ()
+ "A Monday session counts in its week; the Sunday before is the prior week."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let* ((mon test-org-drill-statistics-weekly--this-mon)
+ (log (list (test-org-drill-statistics-weekly--rec mon '(5))
+ (test-org-drill-statistics-weekly--rec (1- mon) '(4))))
+ (agg (org-drill-statistics--weekly-aggregates log 2))
+ (prior (car agg))
+ (this (cadr agg)))
+ (should (= (- mon 7) (plist-get prior :week-start)))
+ (should (= 1 (plist-get prior :reviews)))
+ (should (= mon (plist-get this :week-start)))
+ (should (= 1 (plist-get this :reviews))))))
+
+;;; ---- Error cases ----
+
+(ert-deftest test-org-drill-statistics-weekly-non-positive-weeks-errors ()
+ "WEEKS below 1 signals an error."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (should-error (org-drill-statistics--weekly-aggregates nil 0))
+ (should-error (org-drill-statistics--weekly-aggregates nil -3))))
+
+(ert-deftest test-org-drill-statistics-weekly-empty-qualities-record ()
+ "A record with no qualities adds 0 reviews but still counts toward the
+week's average duration."
+ (test-org-drill-statistics-weekly--with-today
+ test-org-drill-statistics-weekly--today
+ (let* ((log (list (test-org-drill-statistics-weekly--rec
+ test-org-drill-statistics-weekly--today [] 10)))
+ (agg (org-drill-statistics--weekly-aggregates log 1))
+ (this (car agg)))
+ (should (= 0 (plist-get this :reviews)))
+ (should (= 0 (plist-get this :pass-percent)))
+ (should (= 10.0 (plist-get this :avg-duration-min))))))
+
+(provide 'test-org-drill-statistics-weekly-aggregates)
+
+;;; test-org-drill-statistics-weekly-aggregates.el ends here