diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 08:35:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 08:35:16 -0500 |
| commit | 26cc4472dea261a1ad13fbee8fb6a91b019f77bb (patch) | |
| tree | a7063337b05c3ea278a5b910d0f1420de033dfe8 /tests/test-org-drill-statistics-weekly-aggregates.el | |
| parent | 532ce532465834ce06238648ba1490c48bed29ca (diff) | |
| download | org-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.el | 184 |
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 |
