#+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 ?" 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: