489 lines
19 KiB
Org Mode
489 lines
19 KiB
Org Mode
#+PROPERTY: header-args :tangle yes :load yes :comments link
|
||
#+BEGIN_SRC emacs-lisp :load no :tangle no
|
||
(setq nameless-current-name "chronometrist")
|
||
#+END_SRC
|
||
|
||
* Setup
|
||
** test-file-path-stem
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar chronometrist-test-file-path-stem
|
||
(format "%stest" (file-name-directory (or load-file-name default-directory))))
|
||
#+END_SRC
|
||
|
||
** test-backends
|
||
=chronometrist-test-backend= is just here till I finish writing the tests for a single backend. Will tackle how to apply the same tests to different backends afterwards - possibly with a [[#chronometrist-ert-deftest][wrapper macro]] around =ert-deftest=.
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar chronometrist-test-backend
|
||
(make-instance 'chronometrist-plist-group-backend :path chronometrist-test-file-path-stem))
|
||
|
||
(defun chronometrist-make-test-backends ()
|
||
(cl-loop for backend in '(chronometrist-plist-backend chronometrist-plist-group-backend)
|
||
collect (make-instance backend :path chronometrist-test-file-path-stem)))
|
||
|
||
(defvar chronometrist-test-backends (chronometrist-make-test-backends))
|
||
#+END_SRC
|
||
|
||
** test-first-record
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar chronometrist-test-first-record
|
||
'(:name "Programming"
|
||
:start "2018-01-01T00:00:00+0530"
|
||
:stop "2018-01-01T01:00:00+0530"))
|
||
#+END_SRC
|
||
|
||
** test-latest-record
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar chronometrist-test-latest-record
|
||
'(:name "Programming"
|
||
:tags (reading)
|
||
:book "Smalltalk-80: The Language and Its Implementation"
|
||
:start "2020-05-10T16:33:17+0530"
|
||
:stop "2020-05-10T17:10:48+0530"))
|
||
#+END_SRC
|
||
|
||
** test-records
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar chronometrist-test-records
|
||
'((:name "Programming"
|
||
:start "2018-01-01T00:00:00+0530"
|
||
:stop "2018-01-01T01:00:00+0530")
|
||
(:name "Swimming"
|
||
:start "2018-01-01T02:00:00+0530"
|
||
:stop "2018-01-01T03:00:00+0530")
|
||
(:name "Cooking"
|
||
:start "2018-01-01T04:00:00+0530"
|
||
:stop "2018-01-01T05:00:00+0530")
|
||
(:name "Guitar"
|
||
:start "2018-01-01T06:00:00+0530"
|
||
:stop "2018-01-01T07:00:00+0530")
|
||
(:name "Cycling"
|
||
:start "2018-01-01T08:00:00+0530"
|
||
:stop "2018-01-01T09:00:00+0530")
|
||
(:name "Programming"
|
||
:start "2018-01-02T23:00:00+0530"
|
||
:stop "2018-01-03T01:00:00+0530")
|
||
(:name "Cooking"
|
||
:start "2018-01-03T23:00:00+0530"
|
||
:stop "2018-01-04T01:00:00+0530")
|
||
(:name "Programming"
|
||
:tags (bug-hunting)
|
||
:project "Chronometrist"
|
||
:component "goals"
|
||
:start "2020-05-09T20:03:25+0530"
|
||
:stop "2020-05-09T20:05:55+0530")
|
||
(:name "Arrangement/new edition"
|
||
:tags (new edition)
|
||
:song "Songs of Travel"
|
||
:composer "Vaughan Williams, Ralph"
|
||
:start "2020-05-10T00:04:14+0530"
|
||
:stop "2020-05-10T00:25:48+0530")
|
||
(:name "Guitar"
|
||
:tags (classical warm-up)
|
||
:start "2020-05-10T15:41:14+0530"
|
||
:stop "2020-05-10T15:55:42+0530")
|
||
(:name "Guitar"
|
||
:tags (classical solo)
|
||
:start "2020-05-10T16:00:00+0530"
|
||
:stop "2020-05-10T16:30:00+0530")
|
||
(:name "Programming"
|
||
:tags (reading)
|
||
:book "Smalltalk-80: The Language and Its Implementation"
|
||
:start "2020-05-10T16:33:17+0530"
|
||
:stop "2020-05-10T17:10:48+0530")))
|
||
#+END_SRC
|
||
|
||
** cleanup
|
||
#+BEGIN_SRC emacs-lisp
|
||
(cl-defgeneric chronometrist-backend-test-cleanup (backend)
|
||
"Delete any files created by BACKEND during testing.")
|
||
|
||
(cl-defmethod chronometrist-backend-test-cleanup ((backend chronometrist-elisp-sexp-backend))
|
||
(with-slots (file hash-table) backend
|
||
(when file
|
||
(when (file-exists-p file)
|
||
(delete-file file))
|
||
(with-current-buffer (get-file-buffer file)
|
||
;; (erase-buffer)
|
||
;; Unsuccessful attempt to inhibit "Delete excess backup versions of <file>?" prompts
|
||
(defvar delete-old-versions)
|
||
(let ((delete-old-versions t))
|
||
(set-buffer-modified-p nil)
|
||
(kill-buffer))))
|
||
(setf hash-table (clrhash hash-table))))
|
||
|
||
(cl-defmethod chronometrist-backend-test-cleanup :after ((backend t))
|
||
(setf chronometrist-test-backends (chronometrist-make-test-backends)))
|
||
#+END_SRC
|
||
|
||
** ert-deftest :macro:
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: chronometrist-ert-deftest
|
||
:END:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defmacro chronometrist-ert-deftest (name backend-var &rest test-forms)
|
||
"Generate test groups containing TEST-FORMS for each backend.
|
||
BACKEND-VAR is bound to each backend in
|
||
`chronometrist-test-backends'. TEST-FORMS are passed to
|
||
`ert-deftest'."
|
||
(declare (indent defun) (debug t))
|
||
(cl-loop for backend in chronometrist-test-backends collect
|
||
(let* ((backend-name (string-remove-suffix
|
||
"-backend"
|
||
(string-remove-prefix "chronometrist"
|
||
(symbol-name
|
||
(eieio-object-class-name backend)))))
|
||
(test-name (concat "chronometrist-" (symbol-name name) backend-name)))
|
||
`(ert-deftest ,(intern test-name) ()
|
||
(let ((,backend-var ,backend))
|
||
(unwind-protect
|
||
(progn ,@test-forms)
|
||
;; cleanup - remove test backend file
|
||
(chronometrist-backend-test-cleanup ,backend)))))
|
||
into test-groups
|
||
finally return (cons 'progn test-groups)))
|
||
#+END_SRC
|
||
|
||
* Tests
|
||
** common
|
||
*** current-task
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: tests-common-current-task
|
||
:END:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(chronometrist-ert-deftest current-task b
|
||
;; (message "current-task test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||
(chronometrist-create-file b)
|
||
(should (not (chronometrist-current-task b)))
|
||
(chronometrist-insert b (list :name "Test" :start (chronometrist-format-time-iso8601)))
|
||
(should (equal "Test" (chronometrist-current-task b)))
|
||
(chronometrist-remove-last b)
|
||
(should (not (chronometrist-current-task b))))
|
||
#+END_SRC
|
||
|
||
*** plist-p
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: tests-common-plist-p
|
||
:END:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ert-deftest chronometrist-plist-p ()
|
||
(should (eq t (chronometrist-plist-p '(:a 1 :b 2))))
|
||
(should (eq nil (chronometrist-plist-p '(0 :a 1 :b 2))))
|
||
(should (eq nil (chronometrist-plist-p '(:a 1 :b 2 3))))
|
||
(should (not (chronometrist-plist-p nil))))
|
||
#+END_SRC
|
||
|
||
*** plists-split-p
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: tests-common-plists-split-p
|
||
:END:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ert-deftest chronometrist-plists-split-p ()
|
||
(should
|
||
(chronometrist-plists-split-p
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-11-30T23:01:10+0530"
|
||
:stop "2021-12-01T00:00:00+0530")
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-12-01T00:00:00+0530"
|
||
:stop "2021-12-01T00:06:22+0530")))
|
||
;; without :stop
|
||
(should
|
||
(chronometrist-plists-split-p
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-11-30T23:01:10+0530"
|
||
:stop "2021-12-01T00:00:00+0530")
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-12-01T00:00:00+0530")))
|
||
;; difference in time
|
||
(should
|
||
(not (chronometrist-plists-split-p
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-11-30T23:01:10+0530"
|
||
:stop "2021-12-01T00:00:00+0530")
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-12-01T00:00:01+0530"
|
||
:stop "2021-12-01T00:06:22+0530"))))
|
||
;; difference in key-values
|
||
(should
|
||
(not (chronometrist-plists-split-p
|
||
'(:name "Cooking"
|
||
:recipe "whole wheat penne rigate in arrabbiata sauce"
|
||
:start "2021-11-30T23:01:10+0530"
|
||
:stop "2021-12-01T00:00:00+0530")
|
||
'(:name "Cooking"
|
||
:start "2021-12-01T00:00:00+0530"
|
||
:stop "2021-12-01T00:06:22+0530")))))
|
||
#+END_SRC
|
||
|
||
** data structures
|
||
*** list-tasks
|
||
#+BEGIN_SRC emacs-lisp
|
||
(chronometrist-ert-deftest list-tasks b
|
||
;; (message "list-tasks test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||
(chronometrist-create-file b)
|
||
(let ((task-list (chronometrist-list-tasks b)))
|
||
(should (listp task-list))
|
||
(should (seq-every-p #'stringp task-list))))
|
||
#+END_SRC
|
||
|
||
** time functions
|
||
*** format-duration-long :pure:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ert-deftest chronometrist-format-duration-long ()
|
||
(should (equal (chronometrist-format-duration-long 5) ""))
|
||
(should (equal (chronometrist-format-duration-long 65) "1 minute"))
|
||
(should (equal (chronometrist-format-duration-long 125) "2 minutes"))
|
||
|
||
(should (equal (chronometrist-format-duration-long 3605) "1 hour"))
|
||
(should (equal (chronometrist-format-duration-long 3660) "1 hour, 1 minute"))
|
||
(should (equal (chronometrist-format-duration-long 3725) "1 hour, 2 minutes"))
|
||
|
||
(should (equal (chronometrist-format-duration-long 7200) "2 hours"))
|
||
(should (equal (chronometrist-format-duration-long 7260) "2 hours, 1 minute"))
|
||
(should (equal (chronometrist-format-duration-long 7320) "2 hours, 2 minutes")))
|
||
#+END_SRC
|
||
|
||
** plist pretty-printing
|
||
[[file:../elisp/chronometrist.org::#program-pretty-printer][source]]
|
||
|
||
*** plist-group-p
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ert-deftest chronometrist-plist-group-p ()
|
||
(should (eq t (chronometrist-plist-group-p '(symbol (:a 1 :b 2)))))
|
||
(should (eq t (chronometrist-plist-group-p '("string" (:a 1 :b 2)))))
|
||
(should (not (chronometrist-plist-group-p nil)))
|
||
(should (not (chronometrist-plist-group-p '("string")))))
|
||
#+END_SRC
|
||
|
||
*** plist-pp-to-string
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ert-deftest chronometrist-pp-to-string ()
|
||
(should
|
||
(equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Task"
|
||
:tags (foo bar)
|
||
:comment ((70 . "baz")
|
||
"zot"
|
||
(16 . "frob")
|
||
(20 20 "quux"))
|
||
:start "2020-06-25T19:27:57+0530"
|
||
:stop "2020-06-25T19:43:30+0530"))
|
||
(concat
|
||
"(:name \"Task\"\n"
|
||
" :tags (foo bar)\n"
|
||
" :comment ((70 . \"baz\")\n"
|
||
" \"zot\"\n"
|
||
" (16 . \"frob\")\n"
|
||
" (20 20 \"quux\"))\n"
|
||
" :start \"2020-06-25T19:27:57+0530\"\n"
|
||
" :stop \"2020-06-25T19:43:30+0530\")")))
|
||
(should
|
||
(equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Singing"
|
||
:tags (classical solo)
|
||
:piece ((:composer "Gioachino Rossini"
|
||
:name "Il barbiere di Siviglia"
|
||
:aria ("All'idea di quel metallo" "Dunque io son"))
|
||
(:composer "Ralph Vaughan Williams"
|
||
:name "Songs of Travel"
|
||
:movement ((4 . "Youth and Love")
|
||
(5 . "In Dreams")
|
||
(7 . "Wither Must I Wander?")))
|
||
(:composer "Ralph Vaughan Williams"
|
||
:name "Merciless Beauty"
|
||
:movement 1)
|
||
(:composer "Franz Schubert"
|
||
:name "Winterreise"
|
||
:movement ((1 . "Gute Nacht")
|
||
(2 . "Die Wetterfahne")
|
||
(4 . "Erstarrung"))))
|
||
:start "2020-11-01T12:01:20+0530"
|
||
:stop "2020-11-01T13:08:32+0530"))
|
||
(concat
|
||
"(:name \"Singing\"\n"
|
||
" :tags (classical solo)\n"
|
||
" :piece ((:composer \"Gioachino Rossini\"\n"
|
||
" :name \"Il barbiere di Siviglia\"\n"
|
||
" :aria (\"All'idea di quel metallo\" \"Dunque io son\"))\n"
|
||
" (:composer \"Ralph Vaughan Williams\"\n"
|
||
" :name \"Songs of Travel\"\n"
|
||
" :movement ((4 . \"Youth and Love\")\n"
|
||
" (5 . \"In Dreams\")\n"
|
||
" (7 . \"Wither Must I Wander?\")))\n"
|
||
" (:composer \"Ralph Vaughan Williams\"\n"
|
||
" :name \"Merciless Beauty\"\n"
|
||
" :movement 1)\n"
|
||
" (:composer \"Franz Schubert\"\n"
|
||
" :name \"Winterreise\"\n"
|
||
" :movement ((1 . \"Gute Nacht\")\n"
|
||
" (2 . \"Die Wetterfahne\")\n"
|
||
" (4 . \"Erstarrung\"))))\n"
|
||
" :start \"2020-11-01T12:01:20+0530\"\n"
|
||
" :stop \"2020-11-01T13:08:32+0530\")")))
|
||
(should (equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Cooking"
|
||
:tags (lunch)
|
||
:recipe (:name "moong-masoor ki dal"
|
||
:url "https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/")
|
||
:start "2020-09-23T15:22:39+0530"
|
||
:stop "2020-09-23T16:29:49+0530"))
|
||
(concat
|
||
"(:name \"Cooking\"\n"
|
||
" :tags (lunch)\n"
|
||
" :recipe (:name \"moong-masoor ki dal\"\n"
|
||
" :url \"https://www.mirchitales.com/moong-masoor-dal-red-and-yellow-lentil-curry/\")\n"
|
||
" :start \"2020-09-23T15:22:39+0530\"\n"
|
||
" :stop \"2020-09-23T16:29:49+0530\")")))
|
||
(should (equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Exercise"
|
||
:tags (warm-up)
|
||
:start "2018-11-21T15:35:04+0530"
|
||
:stop "2018-11-21T15:38:41+0530"
|
||
:comment ("stretching" (25 10 "push-ups"))))
|
||
(concat
|
||
"(:name \"Exercise\"\n"
|
||
" :tags (warm-up)\n"
|
||
" :start \"2018-11-21T15:35:04+0530\"\n"
|
||
" :stop \"2018-11-21T15:38:41+0530\"\n"
|
||
" :comment (\"stretching\" (25 10 \"push-ups\")))")))
|
||
(should (equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Guitar"
|
||
:tags (classical)
|
||
:warm-up ((right-hand-patterns "pima" "piam" "pmia" "pmai" "pami" "paim"))
|
||
:start "2021-09-28T17:49:18+0530"
|
||
:stop "2021-09-28T17:53:49+0530"))
|
||
(concat
|
||
"(:name \"Guitar\"\n"
|
||
" :tags (classical)\n"
|
||
" :warm-up ((right-hand-patterns \"pima\" \"piam\" \"pmia\" \"pmai\" \"pami\" \"paim\"))\n"
|
||
" :start \"2021-09-28T17:49:18+0530\"\n"
|
||
" :stop \"2021-09-28T17:53:49+0530\")")))
|
||
(should (equal
|
||
(chronometrist-pp-to-string
|
||
'(:name "Cooking"
|
||
:tags (lunch)
|
||
:recipe ("urad dhuli"
|
||
(:name "brown rice"
|
||
:brand "Dawat quick-cooking"
|
||
:quantity "40% of steel measuring glass"
|
||
:water "2× dry rice"))
|
||
:start "2021-11-07T14:40:45+0530"
|
||
:stop "2021-11-07T15:28:13+0530"))
|
||
(concat
|
||
"(:name \"Cooking\"\n"
|
||
" :tags (lunch)\n"
|
||
" :recipe (\"urad dhuli\"\n"
|
||
" (:name \"brown rice\"\n"
|
||
" :brand \"Dawat quick-cooking\"\n"
|
||
" :quantity \"40% of steel measuring glass\"\n"
|
||
" :water \"2× dry rice\"))\n"
|
||
" :start \"2021-11-07T14:40:45+0530\"\n"
|
||
" :stop \"2021-11-07T15:28:13+0530\")"))))
|
||
#+END_SRC
|
||
|
||
** backend
|
||
Situations
|
||
1. no file
|
||
2. empty file
|
||
3. non-empty file with no records
|
||
4. single record
|
||
* active
|
||
* inactive
|
||
* active, day-crossing
|
||
* inactive, day-crossing
|
||
5. multiple records
|
||
6. +[plist-group] latest plist is split+ (covered in #4)
|
||
|
||
Tests to be added -
|
||
1. to-hash-table
|
||
2. to-file
|
||
|
||
The order of these tests is important - the last test for each case is one which moves into the next case.
|
||
|
||
*** create-file
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: tests-backend-create-file
|
||
:END:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(chronometrist-ert-deftest create-file b
|
||
;; (message "create-file test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||
;; * file does not exist *
|
||
(should (chronometrist-create-file b))
|
||
;; * file exists but has no records *
|
||
(should (not (chronometrist-create-file b))))
|
||
#+END_SRC
|
||
|
||
*** latest-date-records
|
||
#+BEGIN_SRC emacs-lisp
|
||
(chronometrist-ert-deftest latest-date-records b
|
||
;; (message "latest-date-records test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||
(let ((plist-1 (cl-first chronometrist-test-records))
|
||
(plist-2 (cl-second chronometrist-test-records))
|
||
(today-ts (chronometrist-date-ts)))
|
||
;; * file does not exist *
|
||
(should-error (chronometrist-latest-date-records b))
|
||
(should (chronometrist-create-file b))
|
||
;; (message "latest-date-records: %S" (chronometrist-latest-date-records b))
|
||
;; * file exists but has no records *
|
||
(should (not (chronometrist-latest-date-records b)))
|
||
(should (chronometrist-insert b plist-1))
|
||
;; * backend has a single active record *
|
||
|
||
;; * backend has a single inactive record *
|
||
;; * backend has a single active day-crossing record *
|
||
;; * backend has a single inactive day-crossing record *
|
||
|
||
;; * backend has two records and one is active *
|
||
;; * backend has two records and both are inactive *
|
||
;; * backend has two day-crossing records and one is active *
|
||
;; * backend has two day-crossing records and both are inactive *
|
||
))
|
||
#+END_SRC
|
||
|
||
*** insert
|
||
#+BEGIN_SRC emacs-lisp
|
||
(chronometrist-ert-deftest insert b
|
||
;; (message "insert test - hash-table-count %s" (hash-table-count (chronometrist-backend-hash-table b)))
|
||
(let* ((plist1 (list :name "Test" :start (chronometrist-format-time-iso8601)))
|
||
(plist2 (append plist1 (list :stop (chronometrist-format-time-iso8601)))))
|
||
;; * file does not exist *
|
||
(should-error (chronometrist-insert b plist1))
|
||
(should (chronometrist-create-file b))
|
||
;; * file exists but has no records *
|
||
(should (chronometrist-insert b plist1))
|
||
(should (equal (progn (chronometrist-reset-backend b)
|
||
(chronometrist-latest-date-records b))
|
||
(list (chronometrist-date-iso) plist1)))
|
||
;; * backend has a single active record *
|
||
(should (chronometrist-replace-last b plist2))
|
||
(should (equal (progn (chronometrist-reset-backend b)
|
||
(chronometrist-latest-date-records b))
|
||
(list (chronometrist-date-iso) plist2)))
|
||
;; * backend has a single inactive record *
|
||
;; * backend has a single active day-crossing record *
|
||
;; * backend has a single inactive day-crossing record *
|
||
|
||
;; * backend has two records and one is active *
|
||
;; * backend has two records and both are inactive *
|
||
;; * backend has two day-crossing records and one is active *
|
||
;; * backend has two day-crossing records and both are inactive *
|
||
))
|
||
#+END_SRC
|
||
|
||
* Local Variables
|
||
# Local Variables:
|
||
# delete-old-versions: t
|
||
# End:
|