This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.
chronometrist/tests/chronometrist-tests.org

19 KiB
Raw Blame History

(setq nameless-current-name "chronometrist")

Setup

test-file-path-stem

(defvar chronometrist-test-file-path-stem
  (format "%stest" (file-name-directory (or load-file-name default-directory))))

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 wrapper macro around ert-deftest.

(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))

test-first-record

(defvar chronometrist-test-first-record
  '(:name  "Programming"
    :start "2018-01-01T00:00:00+0530"
    :stop  "2018-01-01T01:00:00+0530"))

test-latest-record

(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"))

test-records

(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")))

cleanup

(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)))

ert-deftest   macro

(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)))

Tests

common

current-task

(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))))

plist-p

(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))))

plists-split-p

(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")))))

data structures

list-tasks

(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))))

time functions

format-duration-long   pure

(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")))

plist pretty-printing

source

plist-group-p

(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")))))

plist-pp-to-string

(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\")"))))

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

(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))))

latest-date-records

(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 *
    ))

insert

(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 *
    ))

Local Variables